commit 1f268acc29e313f28bfa8dd52cb56dc13d95e73c Author: fanbook-wangdage <124357765+fanbook-wangdage@users.noreply.github.com> Date: Thu Nov 20 20:28:27 2025 +0800 init diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..2f574d6 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "cake.tool": { + "version": "5.1.0", + "commands": [ + "dotnet-cake" + ] + } + } +} \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..82dead9 --- /dev/null +++ b/.github/FUNDING.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/CHS-bug-report.yml b/.github/ISSUE_TEMPLATE/CHS-bug-report.yml new file mode 100644 index 0000000..6586c15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/CHS-bug-report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/CHS-feature-request.yml b/.github/ISSUE_TEMPLATE/CHS-feature-request.yml new file mode 100644 index 0000000..06ba935 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/CHS-feature-request.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/CHS-network-issue.yml b/.github/ISSUE_TEMPLATE/CHS-network-issue.yml new file mode 100644 index 0000000..c30eeed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/CHS-network-issue.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/ENG-bug-report.yml b/.github/ISSUE_TEMPLATE/ENG-bug-report.yml new file mode 100644 index 0000000..217fbfe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENG-bug-report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/ENG-feature-request.yml b/.github/ISSUE_TEMPLATE/ENG-feature-request.yml new file mode 100644 index 0000000..e7ebcae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENG-feature-request.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/ENG-network-issue.yml b/.github/ISSUE_TEMPLATE/ENG-network-issue.yml new file mode 100644 index 0000000..f79f16f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENG-network-issue.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/MGMT-publish.yml b/.github/ISSUE_TEMPLATE/MGMT-publish.yml new file mode 100644 index 0000000..fd2d821 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/MGMT-publish.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3e57011 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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 + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000..f5c57fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -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 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..af2f167 --- /dev/null +++ b/.github/copilot-instructions.md @@ -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. + + +- 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. + + + +- 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. + + +--- + +## 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: Don’t 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 repository’s 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. + +--- diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..77df43d --- /dev/null +++ b/.github/dependabot.yml @@ -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" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..39d04fd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ + + + +## Description + + + +## Related Issue + + + + +## Checklist + +- [ ] The target PR branch is `develop` branch diff --git a/.github/workflows/PublishDistribution.yml b/.github/workflows/PublishDistribution.yml new file mode 100644 index 0000000..ff1a410 --- /dev/null +++ b/.github/workflows/PublishDistribution.yml @@ -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 }' diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml new file mode 100644 index 0000000..e2cba04 --- /dev/null +++ b/.github/workflows/alpha.yml @@ -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." diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 0000000..ab30f08 --- /dev/null +++ b/.github/workflows/canary.yml @@ -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 diff --git a/.github/workflows/close_stale.yml b/.github/workflows/close_stale.yml new file mode 100644 index 0000000..b139dc6 --- /dev/null +++ b/.github/workflows/close_stale.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/issue_similarity.yml b/.github/workflows/issue_similarity.yml new file mode 100644 index 0000000..c484a98 --- /dev/null +++ b/.github/workflows/issue_similarity.yml @@ -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 diff --git a/.github/workflows/lock_closed_issues.yml b/.github/workflows/lock_closed_issues.yml new file mode 100644 index 0000000..47200ad --- /dev/null +++ b/.github/workflows/lock_closed_issues.yml @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c9f84e --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..1e26226 --- /dev/null +++ b/.gitlab-ci.yml @@ -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" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0ce7a27 --- /dev/null +++ b/.vscode/settings.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..decf6ee --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..58982a8 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a83b962 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..3c239e1 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1efcc3c --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ +

+ Snap Hutao Banner +

+ +

Snap Hutao

+ +

+ 🎮 开源的原神工具箱,专为 Windows 平台设计,改善桌面端玩家的游戏体验 +
+ 🎮 An open-source Genshin Impact toolkit for Windows, designed to improve the desktop gaming experience +

+ +
+ + + + + + + + + + + +
+ Latest CI/CD Build + + Latest Release + + Downloads +
+ + Build Status + + + + Release + + + Downloads +
+
+ + +--- + +## 📖 简介 / 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. + +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) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8b08589 --- /dev/null +++ b/SECURITY.md @@ -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) diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..3093888 --- /dev/null +++ b/appveyor.yml @@ -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== diff --git a/build.cake b/build.cake new file mode 100644 index 0000000..94c0a9e --- /dev/null +++ b/build.cake @@ -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 + { + { "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 { { "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); diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..66e78b9 --- /dev/null +++ b/crowdin.yml @@ -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 \ No newline at end of file diff --git a/res/HutaoLogo/Banner3-large-cn.psd b/res/HutaoLogo/Banner3-large-cn.psd new file mode 100644 index 0000000..b963783 Binary files /dev/null and b/res/HutaoLogo/Banner3-large-cn.psd differ diff --git a/res/HutaoLogo/Banner3-large.psd b/res/HutaoLogo/Banner3-large.psd new file mode 100644 index 0000000..35ca0d3 Binary files /dev/null and b/res/HutaoLogo/Banner3-large.psd differ diff --git a/res/HutaoLogo/HutaoIcon2.jpg b/res/HutaoLogo/HutaoIcon2.jpg new file mode 100644 index 0000000..358ca48 Binary files /dev/null and b/res/HutaoLogo/HutaoIcon2.jpg differ diff --git a/res/HutaoLogo/HutaoIcon2.png b/res/HutaoLogo/HutaoIcon2.png new file mode 100644 index 0000000..00fadf4 Binary files /dev/null and b/res/HutaoLogo/HutaoIcon2.png differ diff --git a/res/HutaoLogo/HutaoIconSource.jpg b/res/HutaoLogo/HutaoIconSource.jpg new file mode 100644 index 0000000..2d6e067 Binary files /dev/null and b/res/HutaoLogo/HutaoIconSource.jpg differ diff --git a/res/HutaoLogo/HutaoIconSourceTransparentBackground.png b/res/HutaoLogo/HutaoIconSourceTransparentBackground.png new file mode 100644 index 0000000..3022990 Binary files /dev/null and b/res/HutaoLogo/HutaoIconSourceTransparentBackground.png differ diff --git a/res/HutaoLogo/HutaoIconSourceTransparentBackgroundGradient1.png b/res/HutaoLogo/HutaoIconSourceTransparentBackgroundGradient1.png new file mode 100644 index 0000000..5273dfc Binary files /dev/null and b/res/HutaoLogo/HutaoIconSourceTransparentBackgroundGradient1.png differ diff --git a/res/HutaoLogo/HutaoRepoBanner.png b/res/HutaoLogo/HutaoRepoBanner.png new file mode 100644 index 0000000..58dffbe Binary files /dev/null and b/res/HutaoLogo/HutaoRepoBanner.png differ diff --git a/res/HutaoLogo/HutaoRepoBanner.psd b/res/HutaoLogo/HutaoRepoBanner.psd new file mode 100644 index 0000000..6705e4e Binary files /dev/null and b/res/HutaoLogo/HutaoRepoBanner.psd differ diff --git a/res/HutaoLogo/HutaoRepoBanner2.png b/res/HutaoLogo/HutaoRepoBanner2.png new file mode 100644 index 0000000..9b15d43 Binary files /dev/null and b/res/HutaoLogo/HutaoRepoBanner2.png differ diff --git a/res/HutaoLogo/HutaoRepoBanner2.psd b/res/HutaoLogo/HutaoRepoBanner2.psd new file mode 100644 index 0000000..0f65e69 Binary files /dev/null and b/res/HutaoLogo/HutaoRepoBanner2.psd differ diff --git a/res/HutaoLogo/README.md b/res/HutaoLogo/README.md new file mode 100644 index 0000000..e4e21b0 --- /dev/null +++ b/res/HutaoLogo/README.md @@ -0,0 +1,3 @@ +本文件夹中的所有图片,均由 [DGP Studio](https://github.com/DGP-Studio) 委托 [Bilibili 画画的芦苇](https://space.bilibili.com/274422134) 绘制 + +Copyright © 2023 DGP Studio, All Rights Reserved. \ No newline at end of file diff --git a/res/HutaoLogo2/1.png b/res/HutaoLogo2/1.png new file mode 100644 index 0000000..633c2e5 Binary files /dev/null and b/res/HutaoLogo2/1.png differ diff --git a/res/HutaoLogo2/2.jpg b/res/HutaoLogo2/2.jpg new file mode 100644 index 0000000..d528986 Binary files /dev/null and b/res/HutaoLogo2/2.jpg differ diff --git a/res/HutaoLogo2/2.png b/res/HutaoLogo2/2.png new file mode 100644 index 0000000..bc3a8ce Binary files /dev/null and b/res/HutaoLogo2/2.png differ diff --git a/res/HutaoLogo2/3.png b/res/HutaoLogo2/3.png new file mode 100644 index 0000000..21c301e Binary files /dev/null and b/res/HutaoLogo2/3.png differ diff --git a/res/HutaoLogo2/MSIX Logo.png b/res/HutaoLogo2/MSIX Logo.png new file mode 100644 index 0000000..37ab18b Binary files /dev/null and b/res/HutaoLogo2/MSIX Logo.png differ diff --git a/res/HutaoLogo2/Main logo.psd b/res/HutaoLogo2/Main logo.psd new file mode 100644 index 0000000..066776b Binary files /dev/null and b/res/HutaoLogo2/Main logo.psd differ diff --git a/res/HutaoLogo2/No-padding.png b/res/HutaoLogo2/No-padding.png new file mode 100644 index 0000000..6640a4a Binary files /dev/null and b/res/HutaoLogo2/No-padding.png differ diff --git a/res/HutaoLogo2/No-padding.psd b/res/HutaoLogo2/No-padding.psd new file mode 100644 index 0000000..fd76e97 Binary files /dev/null and b/res/HutaoLogo2/No-padding.psd differ diff --git a/res/HutaoLogo2/README.md b/res/HutaoLogo2/README.md new file mode 100644 index 0000000..40683cb --- /dev/null +++ b/res/HutaoLogo2/README.md @@ -0,0 +1 @@ +Copyright © 2025 DGP Studio, All Rights Reserved. \ No newline at end of file diff --git a/res/Store/chs/abyss.psd b/res/Store/chs/abyss.psd new file mode 100644 index 0000000..2d65cff Binary files /dev/null and b/res/Store/chs/abyss.psd differ diff --git a/res/Store/chs/achievement.psd b/res/Store/chs/achievement.psd new file mode 100644 index 0000000..d56e73d Binary files /dev/null and b/res/Store/chs/achievement.psd differ diff --git a/res/Store/chs/character-data.psd b/res/Store/chs/character-data.psd new file mode 100644 index 0000000..d69b326 Binary files /dev/null and b/res/Store/chs/character-data.psd differ diff --git a/res/Store/chs/lancher.psd b/res/Store/chs/lancher.psd new file mode 100644 index 0000000..223cd98 Binary files /dev/null and b/res/Store/chs/lancher.psd differ diff --git a/res/Store/chs/realtime-notes.psd b/res/Store/chs/realtime-notes.psd new file mode 100644 index 0000000..daf275c Binary files /dev/null and b/res/Store/chs/realtime-notes.psd differ diff --git a/res/Store/chs/wish.psd b/res/Store/chs/wish.psd new file mode 100644 index 0000000..925e641 Binary files /dev/null and b/res/Store/chs/wish.psd differ diff --git a/res/Store/en/abyss.psd b/res/Store/en/abyss.psd new file mode 100644 index 0000000..fe62a72 Binary files /dev/null and b/res/Store/en/abyss.psd differ diff --git a/res/Store/en/achievement.psd b/res/Store/en/achievement.psd new file mode 100644 index 0000000..ddd3480 Binary files /dev/null and b/res/Store/en/achievement.psd differ diff --git a/res/Store/en/character-data.psd b/res/Store/en/character-data.psd new file mode 100644 index 0000000..d26774f Binary files /dev/null and b/res/Store/en/character-data.psd differ diff --git a/res/Store/en/lancher.psd b/res/Store/en/lancher.psd new file mode 100644 index 0000000..c4f1bb2 Binary files /dev/null and b/res/Store/en/lancher.psd differ diff --git a/res/Store/en/realtime-notes.psd b/res/Store/en/realtime-notes.psd new file mode 100644 index 0000000..56a0d83 Binary files /dev/null and b/res/Store/en/realtime-notes.psd differ diff --git a/res/Store/en/wish.psd b/res/Store/en/wish.psd new file mode 100644 index 0000000..76653e4 Binary files /dev/null and b/res/Store/en/wish.psd differ diff --git a/res/assets/readmeSponsors.svg b/res/assets/readmeSponsors.svg new file mode 100644 index 0000000..1e8491f --- /dev/null +++ b/res/assets/readmeSponsors.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/.editorconfig b/src/Snap.Hutao/.editorconfig new file mode 100644 index 0000000..6a99a3b --- /dev/null +++ b/src/Snap.Hutao/.editorconfig @@ -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 \ No newline at end of file diff --git a/src/Snap.Hutao/.vscode/settings.json b/src/Snap.Hutao/.vscode/settings.json new file mode 100644 index 0000000..0ce7a27 --- /dev/null +++ b/src/Snap.Hutao/.vscode/settings.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/src/Snap.Hutao/.vsconfig b/src/Snap.Hutao/.vsconfig new file mode 100644 index 0000000..135b30c --- /dev/null +++ b/src/Snap.Hutao/.vsconfig @@ -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" + ] +} \ No newline at end of file diff --git a/src/Snap.Hutao/ResXManager.config.xml b/src/Snap.Hutao/ResXManager.config.xml new file mode 100644 index 0000000..ae92990 --- /dev/null +++ b/src/Snap.Hutao/ResXManager.config.xml @@ -0,0 +1,6 @@ + + + zh-Hans + True + Ordinal + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/.github/workflows/Publish.yml b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/.github/workflows/Publish.yml new file mode 100644 index 0000000..7aa8096 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/.github/workflows/Publish.yml @@ -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 }} diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/.gitignore b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/.gitignore new file mode 100644 index 0000000..4d48eca --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/.gitignore @@ -0,0 +1,3 @@ + +src/Snap.Hutao.SourceGeneration/.idea/ +*.user diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/README.md b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/README.md new file mode 100644 index 0000000..b5817d5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/README.md @@ -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`'s step result should be an `IEquatable` 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` if possible, the pipeline use IA internally and has special check for it. \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/.github/copilot-instructions.md b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/.github/copilot-instructions.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/.gitignore b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/.gitignore new file mode 100644 index 0000000..1b9a42d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/.gitignore @@ -0,0 +1,3 @@ +.vs +Snap.Hutao.SourceGeneration/bin +Snap.Hutao.SourceGeneration/obj \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.sln b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.sln new file mode 100644 index 0000000..472080f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.sln @@ -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 diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/AttributeGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/AttributeGenerator.cs new file mode 100644 index 0000000..ce9f24b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/AttributeGenerator.cs @@ -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(FileScopedNamespaceDeclaration("Snap.Hutao.Core.Annotation") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(List( + [ + ClassDeclaration(Identifier("CommandAttribute")) + .WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsMethod, inherited: false))) + .WithModifiers(InternalSealedTokenList) + .WithBaseList(SystemAttributeBaseList) + .WithMembers(List( + [ + 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( + [ + 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( + [ + 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(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(List( + [ + ClassDeclaration(Identifier("HttpClientAttribute")) + .WithAttributeLists(List( + [ + JetBrainsAnnotationsMeansImplicitUseAttributeList, + SystemAttributeUsageList(AttributeTargetsClass, inherited: false) + ])) + .WithModifiers(InternalSealedTokenList) + .WithBaseList(SystemAttributeBaseList) + .WithMembers(List( + [ + 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( + [ + 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(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection.Annotation") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(List( + [ + ClassDeclaration(Identifier("ServiceAttribute")) + .WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsClass, inherited: false))) + .WithModifiers(InternalSealedTokenList) + .WithBaseList(SystemAttributeBaseList).WithMembers(List( + [ + 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( + 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(FileScopedNamespaceDeclaration("Snap.Hutao.Resource.Localization") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(List( + [ + ClassDeclaration(Identifier("ExtendedEnumAttribute")) + .WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsEnum))) + .WithModifiers(InternalSealedTokenList) + .WithBaseList(SystemAttributeBaseList), + ClassDeclaration(Identifier("LocalizationKeyAttribute")) + .WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsField))) + .WithModifiers(InternalSealedTokenList) + .WithBaseList(SystemAttributeBaseList) + .WithMembers(SingletonList( + 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(FileScopedNamespaceDeclaration("System.Runtime.CompilerServices") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(SingletonList( + ClassDeclaration(Identifier("InterceptsLocationAttribute")) + .WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsMethod, allowMultiple: true))) + .WithModifiers(InternalSealedTokenList) + .WithBaseList(SystemAttributeBaseList) + .WithMembers(SingletonList( + 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 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)))); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/ApiEndpointsGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/ApiEndpointsGenerator.cs new file mode 100644 index 0000000..e4cf3c1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/ApiEndpointsGenerator.cs @@ -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 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(FileScopedNamespaceDeclaration(context.ExtraInfo?.Namespace ?? "Snap.Hutao.Web") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers( + List( + [ + InterfaceDeclaration(interfaceName) + .WithModifiers(InternalPartialTokenList) + .WithMembers(List(GenerateInterfaceMethods(context.Endpoints))), + ClassDeclaration(chineseImplName) + .WithModifiers(InternalAbstractTokenList) + .WithBaseList(BaseList(SingletonSeparatedList(SimpleBaseType(interfaceIdentifier)))) + .WithMembers(List(GenerateClassMethods(context.Endpoints, true))), + ClassDeclaration(overseaImplName) + .WithModifiers(InternalAbstractTokenList) + .WithBaseList(BaseList(SingletonSeparatedList(SimpleBaseType(interfaceIdentifier)))) + .WithMembers(List(GenerateClassMethods(context.Endpoints, false))) + ])))) + .NormalizeWhitespace(); + + production.AddSource($"{context.Name}.g.cs", compilation.ToFullStringWithHeader()); + } + + private static IEnumerable GenerateInterfaceMethods(ImmutableArray metadataArray) + { + foreach (EndpointsMetadata metadata in metadataArray) + { + if (metadata.GetMethodDeclaration() is not MethodDeclarationSyntax methodDeclaration) + { + continue; + } + + string lead = $""" + /// + /// CN: {metadata.Chinese?.Replace("&", "&")} + /// OS: {metadata.Oversea?.Replace("&", "&")} + /// + + """; + + yield return methodDeclaration + .WithLeadingTrivia(ParseLeadingTrivia(lead)) + .WithSemicolonToken(SemicolonToken); + } + } + + private static IEnumerable GenerateClassMethods(ImmutableArray 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 ParseCsvLine(string line) + { + List fields = []; + StringBuilder currentField = new(); + bool insideQuotes = false; + + ReadOnlySpan 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 Endpoints { get; init; } + + public static EndpointsMetadataContext Create(AdditionalText text, CancellationToken token) + { + string fileName = Path.GetFileNameWithoutExtension(text.Path); + + EndpointsExtraInfo? extraInfo = default; + ImmutableArray.Builder endpointsBuilder = ImmutableArray.CreateBuilder(); + 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(line[6..]); + continue; + } + + IReadOnlyList 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 + { + 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; } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/ConstructorGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/ConstructorGenerator.cs new file mode 100644 index 0000000..0c9c49c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/ConstructorGenerator.cs @@ -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 provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.GeneratedConstructorAttribute, + SyntaxNodeHelper.Is, + 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 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 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 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 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 Parameters { get; init; } + + public required EquatableArray<(bool ShouldSkip, FieldInfo Field)> Fields { get; init; } + + public required EquatableArray Properties { get; init; } + + public required EquatableArray 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.Builder propertiesBuilder = ImmutableArray.CreateBuilder(); + + 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.Builder interfacesBuilder = ImmutableArray.CreateBuilder(); + 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, + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/UnsafePropertyBackingFieldAccessorGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/UnsafePropertyBackingFieldAccessorGenerator.cs new file mode 100644 index 0000000..5e3f0a2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Automation/UnsafePropertyBackingFieldAccessorGenerator.cs @@ -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 provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.FieldAccessAttribute, + SyntaxNodeHelper.Is, + 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 GenerateAccessMethods(FieldAccessorGeneratorContext context, CancellationToken token) + { + ImmutableArray.Builder accessMethodsBuilder = ImmutableArray.CreateBuilder(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( + [ + 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()) + { + 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 Properties { get; init; } + + public static FieldAccessorGeneratorContext Create((HierarchyInfo Hierarchy, EquatableArray Properties) tuple, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + return new() + { + Hierarchy = tuple.Hierarchy, + Properties = tuple.Properties, + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs new file mode 100644 index 0000000..22f21ae --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs @@ -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 provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.HttpClientAttribute, + SyntaxNodeHelper.Is, + 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(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(SingletonList( + ClassDeclaration("ServiceCollectionExtension") + .WithModifiers(InternalStaticPartialTokenList) + .WithMembers(SingletonList( + 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 GenerateAddHttpClients(HttpClientGeneratorContext context) + { + foreach (HttpClientEntry entry in context.HttpClients) + { + TypeSyntax targetType = ParseTypeName(entry.Type.FullyQualifiedName); + SeparatedSyntaxList 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 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 + { + 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 HttpClients { get; init; } + + public static HttpClientGeneratorContext Create(ImmutableArray services, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + return new() + { + HttpClients = services.Sort(), + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/DependencyInjection/ServiceGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/DependencyInjection/ServiceGenerator.cs new file mode 100644 index 0000000..0f50e48 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/DependencyInjection/ServiceGenerator.cs @@ -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 provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.ServiceAttribute, + SyntaxNodeHelper.Is, + 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(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(SingletonList( + ClassDeclaration("ServiceCollectionExtension") + .WithModifiers(InternalStaticPartialTokenList) + .WithMembers(SingletonList( + 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 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 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 + { + 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 Services { get; init; } + + public static ServiceGeneratorContext Create(ImmutableArray services, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + return new() + { + Services = services.Sort(), + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Enum/ExtendedEnumGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Enum/ExtendedEnumGenerator.cs new file mode 100644 index 0000000..10284b4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Enum/ExtendedEnumGenerator.cs @@ -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 provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.ExtendedEnumAttribute, + SyntaxNodeHelper.Is, + 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(FileScopedNamespaceDeclaration("Snap.Hutao.Resource.Localization") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(SingletonList( + ClassDeclaration($"{context.Type.Name}Extension") + .WithModifiers(InternalStaticPartialTokenList) + .WithMembers(List( + [ + // public static string? GetName(this T value) + MethodDeclaration(NullableStringType, "GetName") + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(enumType, Identifier("value")).WithModifiers(ThisTokenList)))) + .WithBody(Block(SingletonList( + 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( + [ + 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( + 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( + 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( + 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 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 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() + .Select(static field => (FieldInfo.Create(field), AttributeInfo.CreateOrDefault(field.GetAttributes().SingleOrDefault(static data => data.AttributeClass?.HasFullyQualifiedMetadataName(WellKnownMetadataNames.LocalizationKeyAttribute) is true)))) + .ToImmutableArray(), + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/AccessibilityExtension.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/AccessibilityExtension.cs new file mode 100644 index 0000000..ccb264c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/AccessibilityExtension.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/AttributeDataExtension.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/AttributeDataExtension.cs new file mode 100644 index 0000000..9e3bf45 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/AttributeDataExtension.cs @@ -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(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.Default.Equals(argumentValue, value); + } + } + + return false; + } + + [Obsolete] + public static bool HasNamedArgument(this AttributeData attributeData, string name, Func 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(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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/IncrementalValuesProviderExtension.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/IncrementalValuesProviderExtension.cs new file mode 100644 index 0000000..e9b2dc2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/IncrementalValuesProviderExtension.cs @@ -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 Right)> GroupBy( + this IncrementalValuesProvider<(TLeft Left, TRight Right)> source, + Func<(TLeft Left, TRight Right), TKey> keySelector, + Func<(TLeft Left, TRight Right), TElement> elementSelector) + where TLeft : IEquatable + where TRight : IEquatable + where TKey : IEquatable + where TElement : IEquatable + { + return source.Collect().SelectMany((item, token) => + { + Dictionary.Builder> map = new(); + + foreach ((TLeft, TRight) pair in item) + { + TKey key = keySelector(pair); + TElement element = elementSelector(pair); + + if (!map.TryGetValue(key, out ImmutableArray.Builder builder)) + { + builder = ImmutableArray.CreateBuilder(); + + map.Add(key, builder); + } + + builder.Add(element); + } + + token.ThrowIfCancellationRequested(); + + ImmutableArray<(TKey Key, EquatableArray Elements)>.Builder result = + ImmutableArray.CreateBuilder<(TKey, EquatableArray)>(); + + foreach (KeyValuePair.Builder> entry in map) + { + result.Add((entry.Key, entry.Value.ToImmutable())); + } + + return result; + }); + } + + public static IncrementalValuesProvider<(TKey Key, EquatableArray Right)> GroupBy( + this IncrementalValuesProvider source, + Func keySelector) + where TKey : IEquatable + where TElement : IEquatable + { + return source.Collect().SelectMany((item, token) => + { + Dictionary.Builder> map = new(); + + foreach (TElement source in item) + { + TKey key = keySelector(source); + + if (!map.TryGetValue(key, out ImmutableArray.Builder builder)) + { + builder = ImmutableArray.CreateBuilder(); + + map.Add(key, builder); + } + + builder.Add(source); + } + + token.ThrowIfCancellationRequested(); + + ImmutableArray<(TKey Key, EquatableArray Elements)>.Builder result = + ImmutableArray.CreateBuilder<(TKey, EquatableArray)>(); + + foreach (KeyValuePair.Builder> entry in map) + { + result.Add((entry.Key, entry.Value.ToImmutable())); + } + + return result; + }); + } + + public static IncrementalValuesProvider Distinct(this IncrementalValuesProvider source) + where T : IEquatable + { + return source.Collect().SelectMany((array, token) => array.Distinct().ToImmutableArray()); + } + + public static IncrementalValuesProvider Concat(this IncrementalValuesProvider source, IncrementalValuesProvider other) + { + return source.Collect().Combine(other.Collect()).SelectMany(static (t, token) => t.Left.AddRange(t.Right)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/KeyValuePairExtension.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/KeyValuePairExtension.cs new file mode 100644 index 0000000..aff3675 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/KeyValuePairExtension.cs @@ -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(this KeyValuePair pair, out TKey key, out TValue value) + { + key = pair.Key; + value = pair.Value; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/SymbolExtension.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/SymbolExtension.cs new file mode 100644 index 0000000..19df094 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/SymbolExtension.cs @@ -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 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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/SyntaxNodeExtension.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/SyntaxNodeExtension.cs new file mode 100644 index 0000000..6bc0891 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/SyntaxNodeExtension.cs @@ -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()} + """; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/TypeSymbolExtension.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/TypeSymbolExtension.cs new file mode 100644 index 0000000..f8e9317 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Extension/TypeSymbolExtension.cs @@ -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 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 builder = ImmutableArrayBuilder.Rent(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.WrittenSpan.SequenceEqual(name.AsSpan()); + } + + public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol) + { + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.ToString(); + } + + private static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, in ImmutableArrayBuilder builder) + { + static void BuildFrom(ISymbol? symbol, in ImmutableArrayBuilder 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); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/GlobalSuppressions.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/GlobalSuppressions.cs new file mode 100644 index 0000000..7d45814 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/GlobalSuppressions.cs @@ -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")] diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs new file mode 100644 index 0000000..85333a1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs @@ -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 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 ToMetadata(AdditionalText text, CancellationToken token) + { + string identityJson = text.GetText(token)!.ToString(); + return JsonSerializer.Deserialize>(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($""" + /// + /// {metadata.Documentation} + /// + + """); + + IdentifierNameSyntax typeName = IdentifierName(metadataName); + SyntaxToken typeToken = Identifier(metadataName); + + CompilationUnitSyntax syntax = CompilationUnit() + .WithMembers(SingletonList(FileScopedNamespaceDeclaration("Snap.Hutao.Model.Primitive") + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(SingletonList( + StructDeclaration(metadataName) + .WithAttributeLists(SingletonList( + AttributeList(SingletonSeparatedList( + Attribute(NameOfSystemTextJsonSerializationJsonConverter) + .WithArgumentList(AttributeArgumentList(SingletonSeparatedList( + AttributeArgument(GenerateTypeOfIdentityConverterGenericExpression(metadataName))))))) + .WithLeadingTrivia(trivia))) + .WithModifiers(InternalReadOnlyPartialTokenList) + .WithBaseList(BaseList(SeparatedList( + [ + SimpleBaseType(TypeOfSystemIComparable), // System.IComparable + SimpleBaseType(GenerateGenericType(NameOfSystem, "IComparable", typeName)), // System.IComparable + SimpleBaseType(GenerateGenericType(NameOfSystem, "IEquatable", typeName)), // System.IEquatable + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IEqualityOperators", typeName, typeName, BoolType)), // System.Numerics.IEqualityOperators + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IEqualityOperators", typeName, UIntType, BoolType)), // System.Numerics.IEqualityOperators + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IAdditionOperators", typeName, typeName, typeName)), // System.Numerics.IAdditionOperators + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IAdditionOperators", typeName, UIntType, typeName)), // System.Numerics.IAdditionOperators + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "ISubtractionOperators", typeName, typeName, typeName)), // System.Numerics.ISubtractionOperators + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "ISubtractionOperators", typeName, UIntType, typeName)), // System.Numerics.ISubtractionOperators + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IIncrementOperators", typeName)), // System.Numerics.IIncrementOperators + SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IDecrementOperators", typeName)) // System.Numerics.IDecrementOperators + ]))) + .WithMembers(List( + [ + // 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( + [ + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + PlusPlusOperatorDeclaration(typeName) + .WithModifiers(PublicStaticUnsafeTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(typeName, Identifier("value"))))) + .WithBody(Block(List( + [ + ExpressionStatement( + PreIncrementExpression(PointerIndirectionExpression(CastExpression(PointerType(UIntType), AddressOfExpression(IdentifierName("value")))))), + ReturnStatement(IdentifierName("value")) + ]))), + + // public static unsafe T operator --(T value) -> System.Numerics.IDecrementOperators + MinusMinusOperatorDeclaration(typeName) + .WithModifiers(PublicStaticUnsafeTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(typeName, Identifier("value"))))) + .WithBody(Block(List( + [ + 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(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([type1, type2, type3])))); + } + + private sealed record IdentityStructMetadata + { + public string? Name { get; set; } + + public string? Documentation { get; set; } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/AttributeInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/AttributeInfo.cs new file mode 100644 index 0000000..7c8db81 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/AttributeInfo.cs @@ -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 TypeArguments { get; init; } + + public required EquatableArray 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 arguments = + ConstructorArguments + .Select(static arg => AttributeArgument(arg.GetSyntax())); + + // Gather the named arguments + IEnumerable 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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/AttributedMethodInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/AttributedMethodInfo.cs new file mode 100644 index 0000000..48d38e3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/AttributedMethodInfo.cs @@ -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 Attributes { get; init; } + + public required MethodInfo Method { get; init; } + + public static AttributedMethodInfo Create((EquatableArray Attributes, MethodInfo Method) tuple) + { + return new() + { + Attributes = tuple.Attributes, + Method = tuple.Method, + }; + } + + public static AttributedMethodInfo Create(EquatableArray attributes, MethodInfo method) + { + return new() + { + Attributes = attributes, + Method = method, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/EquatableArray.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/EquatableArray.cs new file mode 100644 index 0000000..22d993a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/EquatableArray.cs @@ -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 : IEquatable>, IEnumerable + where T : IEquatable +{ + private readonly T[]? array; + + public EquatableArray(ImmutableArray array) + { + this.array = Unsafe.As, 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 array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is EquatableArray 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 AsImmutableArray() + { + return Unsafe.As>(ref Unsafe.AsRef(in this.array)); + } + + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + public ReadOnlySpan AsSpan() + { + return AsImmutableArray().AsSpan(); + } + + public T[] ToArray() + { + return AsImmutableArray().ToArray(); + } + + public ImmutableArray.Enumerator GetEnumerator() + { + return AsImmutableArray().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + public ImmutableArray SelectAsArray(Func selector) + { + return ImmutableArray.CreateRange(AsImmutableArray(), selector); + } + + public T Single() + { + ImmutableArray array = AsImmutableArray(); + if (array.Length != 1) + { + throw new InvalidOperationException("Sequence contains more than one element"); + } + + return array[0]; + } + + public static implicit operator EquatableArray(ImmutableArray array) + { + return FromImmutableArray(array); + } + + public static implicit operator ImmutableArray(EquatableArray array) + { + return array.AsImmutableArray(); + } + + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/FieldInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/FieldInfo.cs new file mode 100644 index 0000000..4207a98 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/FieldInfo.cs @@ -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 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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/HierarchyInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/HierarchyInfo.cs new file mode 100644 index 0000000..2b981d7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/HierarchyInfo.cs @@ -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 Hierarchy { get; init; } + + public static HierarchyInfo Create(INamedTypeSymbol typeSymbol) + { + using ImmutableArrayBuilder hierarchy = ImmutableArrayBuilder.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 memberDeclarations, BaseListSyntax? baseList = null) + { + // Create the partial type declaration with the given member declarations. + // This code produces a class declaration as follows: + // + // partial + // { + // + // } + 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: + // + // + // + return CompilationUnit() + .WithMembers(SingletonList( + typeDeclarationSyntax + .WithLeadingTrivia(NullableEnableTriviaList))); + } + + // Create the compilation unit with disabled warnings, target namespace and generated type. + // This will produce code as follows: + // + // namespace ; + // + return CompilationUnit() + .WithMembers(SingletonList(FileScopedNamespaceDeclaration(Namespace) + .WithMembers(SingletonList(typeDeclarationSyntax + .WithLeadingTrivia(NullableEnableTriviaList))))); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/MethodInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/MethodInfo.cs new file mode 100644 index 0000000..bc3778d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/MethodInfo.cs @@ -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 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, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/ParameterInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/ParameterInfo.cs new file mode 100644 index 0000000..e50a4a3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/ParameterInfo.cs @@ -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)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/PropertyInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/PropertyInfo.cs new file mode 100644 index 0000000..80a00b5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/PropertyInfo.cs @@ -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 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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypeArgumentInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypeArgumentInfo.cs new file mode 100644 index 0000000..974a6ac --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypeArgumentInfo.cs @@ -0,0 +1,33 @@ +// 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; + +namespace Snap.Hutao.SourceGeneration.Model; + +internal sealed record TypeArgumentInfo +{ + public required string MinimallyQualifiedName { get; init; } + + public required string FullyQualifiedTypeName { get; init; } + + public required string FullyQualifiedTypeNameWithNullabilityAnnotations { get; init; } + + public static TypeArgumentInfo Create(ITypeSymbol typeSymbol) + { + return new() + { + MinimallyQualifiedName = typeSymbol.Name, + FullyQualifiedTypeName = typeSymbol.GetFullyQualifiedName(), + FullyQualifiedTypeNameWithNullabilityAnnotations = typeSymbol.GetFullyQualifiedNameWithNullabilityAnnotations(), + }; + } + + public TypeSyntax GetSyntax() + { + return SyntaxFactory.ParseTypeName(FullyQualifiedTypeNameWithNullabilityAnnotations); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypeInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypeInfo.cs new file mode 100644 index 0000000..ea3b167 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypeInfo.cs @@ -0,0 +1,80 @@ +// 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.Collections.Immutable; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords; + +namespace Snap.Hutao.SourceGeneration.Model; + +internal sealed record TypeInfo +{ + public required string FullyQualifiedName { get; init; } + + public required string FullyQualifiedNameWithoutTypeParameters { get; init; } + + public required string FullyQualifiedMetadataName { get; init; } + + public required string MinimallyQualifiedName { get; init; } + + public required string Name { get; init; } + + public required TypeKind Kind { get; init; } + + public required bool IsRecord { get; init; } + + public required EquatableArray TypeArguments { get; init; } + + public static TypeInfo Create(INamedTypeSymbol symbol) + { + return new() + { + FullyQualifiedName = symbol.GetFullyQualifiedName(), + FullyQualifiedNameWithoutTypeParameters = symbol.GetFullyQualifiedNameWithoutTypeParameters(), + FullyQualifiedMetadataName = symbol.GetFullyQualifiedMetadataName(), + MinimallyQualifiedName = symbol.GetMinimallyQualifiedName(), + Name = symbol.Name, + Kind = symbol.TypeKind, + IsRecord = symbol.IsRecord, + TypeArguments = ImmutableArray.CreateRange(symbol.TypeArguments, TypeArgumentInfo.Create), + }; + } + + public TypeDeclarationSyntax GetSyntax() + { + // Create the partial type declaration with the kind. + // This code produces a class declaration as follows: + // + // + // { + // } + // + // Note that specifically for record declarations, we also need to explicitly add the open + // and close brace tokens, otherwise member declarations will not be formatted correctly. + return (Kind, IsRecord) switch + { + (TypeKind.Struct, false) => StructDeclaration(MinimallyQualifiedName), + (TypeKind.Struct, true) => RecordDeclaration(RecordKeyword, MinimallyQualifiedName) + .WithClassOrStructKeyword(StructKeyword) + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)), + (TypeKind.Interface, _) => InterfaceDeclaration(MinimallyQualifiedName), + (TypeKind.Class, true) => RecordDeclaration(RecordKeyword, MinimallyQualifiedName) + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)), + _ => ClassDeclaration(MinimallyQualifiedName) + }; + } + + public TypeSyntax GetTypeSyntax(bool includeTypeArguments = true) + { + return includeTypeArguments || TypeArguments.IsEmpty + ? ParseTypeName(FullyQualifiedName) + : GenericName(FullyQualifiedNameWithoutTypeParameters).WithTypeArgumentList(FastSyntaxFactory.EmptyTypeArgumentList); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypedConstantInfo.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypedConstantInfo.cs new file mode 100644 index 0000000..6f3fb9f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Model/TypedConstantInfo.cs @@ -0,0 +1,201 @@ +// 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 System; +using System.Collections.Immutable; +using System.Globalization; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory; + +namespace Snap.Hutao.SourceGeneration.Model; + +internal abstract record TypedConstantInfo +{ + public static TypedConstantInfo Create(TypedConstant arg) + { + if (arg.IsNull) + { + return new Null(); + } + + if (arg.Kind == TypedConstantKind.Array) + { + string elementTypeName = ((IArrayTypeSymbol)arg.Type!).ElementType.GetFullyQualifiedName(); + ImmutableArray items = ImmutableArray.CreateRange(arg.Values, Create); + return new Array(elementTypeName, items); + } + + return (arg.Kind, arg.Value) switch + { + (TypedConstantKind.Primitive, string text) => new Primitive.String(text), + (TypedConstantKind.Primitive, bool flag) => new Primitive.Boolean(flag), + (TypedConstantKind.Primitive, { } value) => value switch + { + byte b => new Primitive.Of(b), + char c => new Primitive.Of(c), + double d => new Primitive.Of(d), + float f => new Primitive.Of(f), + int i => new Primitive.Of(i), + long l => new Primitive.Of(l), + sbyte sb => new Primitive.Of(sb), + short sh => new Primitive.Of(sh), + uint ui => new Primitive.Of(ui), + ulong ul => new Primitive.Of(ul), + ushort ush => new Primitive.Of(ush), + _ => throw new ArgumentException("Invalid primitive type") + }, + (TypedConstantKind.Type, ITypeSymbol type) => new Type(type.GetFullyQualifiedName(), type is INamedTypeSymbol { IsUnboundGenericType: true }), + (TypedConstantKind.Enum, { } value) => new Enum(arg.Type!.GetFullyQualifiedName(), value), + _ => throw new ArgumentException("Invalid typed constant type"), + }; + } + + public abstract ExpressionSyntax GetSyntax(); + + public sealed record Array : TypedConstantInfo + { + public Array(string fullyQualifiedElementTypeName, EquatableArray items) + { + FullyQualifiedElementTypeName = fullyQualifiedElementTypeName; + Items = items; + } + + public string FullyQualifiedElementTypeName { get; } + + public EquatableArray Items { get; } + + public override ExpressionSyntax GetSyntax() + { + return CollectionExpression(SeparatedList( + Items.SelectAsArray(static c => ExpressionElement(c.GetSyntax())))); + } + } + + public abstract record Primitive : TypedConstantInfo + { + public sealed record String : TypedConstantInfo + { + public String(string Value) + { + this.Value = Value; + } + + public string Value { get; } + + public override ExpressionSyntax GetSyntax() + { + return StringLiteralExpression(Value); + } + } + + public sealed record Boolean : TypedConstantInfo + { + public Boolean(bool value) + { + Value = value; + } + + public bool Value { get; } + + public override ExpressionSyntax GetSyntax() + { + return Value ? TrueLiteralExpression : FalseLiteralExpression; + } + } + + public sealed record Of : TypedConstantInfo + where T : unmanaged, IEquatable + { + public Of(T value) + { + Value = value; + } + + public T Value { get; } + + public override ExpressionSyntax GetSyntax() + { + return NumericLiteralExpression(Value switch + { + byte b => Literal(b), + char c => Literal(c), + + // For doubles, we need to manually format it and always add the trailing "D" suffix. + // This ensures that the correct type is produced if the expression was assigned to + // an object (eg. the literal was used in an attribute object parameter/property). + double d => Literal($"{d.ToString("R", CultureInfo.InvariantCulture)}D", d), + + // For floats, Roslyn will automatically add the "F" suffix, so no extra work is needed + float f => Literal(f), + int i => Literal(i), + long l => Literal(l), + sbyte sb => Literal(sb), + short sh => Literal(sh), + uint ui => Literal(ui), + ulong ul => Literal(ul), + ushort ush => Literal(ush), + _ => throw new ArgumentException("Invalid primitive type") + }); + } + } + } + + public sealed record Type : TypedConstantInfo + { + public Type(string fullyQualifiedTypeName, bool isUnboundGeneric) + { + FullyQualifiedTypeName = fullyQualifiedTypeName; + IsUnboundGeneric = isUnboundGeneric; + } + + public string FullyQualifiedTypeName { get; } + + public bool IsUnboundGeneric { get; } + + public override ExpressionSyntax GetSyntax() + { + return TypeOfExpression(IdentifierName(FullyQualifiedTypeName)); + } + } + + public sealed record Enum : TypedConstantInfo + { + public Enum(string fullyQualifiedTypeName, object value) + { + FullyQualifiedTypeName = fullyQualifiedTypeName; + Value = value; + } + + public string FullyQualifiedTypeName { get; } + + public object Value { get; } + + public override ExpressionSyntax GetSyntax() + { + // We let Roslyn parse the value expression, so that it can automatically handle both positive and negative values. This + // is needed because negative values have a different syntax tree (UnaryMinusExpression holding the numeric expression). + ExpressionSyntax valueExpression = ParseExpression(Value.ToString()); + + // If the value is negative, we have to put parentheses around them (to avoid CS0075 errors) + if (valueExpression is PrefixUnaryExpressionSyntax unaryExpression && unaryExpression.IsKind(SyntaxKind.UnaryMinusExpression)) + { + valueExpression = ParenthesizedExpression(valueExpression); + } + + // Now we can safely return the cast expression for the target enum type (with optional parentheses if needed) + return CastExpression(IdentifierName(FullyQualifiedTypeName), valueExpression); + } + } + + public sealed record Null : TypedConstantInfo + { + public override ExpressionSyntax GetSyntax() + { + return NullLiteralExpression; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.BinaryExpression.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.BinaryExpression.cs new file mode 100644 index 0000000..6c269c9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.BinaryExpression.cs @@ -0,0 +1,44 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static partial class FastSyntaxFactory +{ + public static BinaryExpressionSyntax AddExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression, left, right); + } + + public static BinaryExpressionSyntax AsExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.BinaryExpression(SyntaxKind.AsExpression, left, right); + } + + public static BinaryExpressionSyntax BitwiseOrExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.BinaryExpression(SyntaxKind.BitwiseOrExpression, left, right); + } + + public static BinaryExpressionSyntax CoalesceExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.BinaryExpression(SyntaxKind.CoalesceExpression, left, right); + } + + public static BinaryExpressionSyntax EqualsExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression, left, right); + } + + public static BinaryExpressionSyntax LogicalAndExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.BinaryExpression(SyntaxKind.LogicalAndExpression, left, right); + } + + public static BinaryExpressionSyntax SubtractExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.BinaryExpression(SyntaxKind.SubtractExpression, left, right); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.LiteralExpression.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.LiteralExpression.cs new file mode 100644 index 0000000..a18613c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.LiteralExpression.cs @@ -0,0 +1,39 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static partial class FastSyntaxFactory +{ + public static LiteralExpressionSyntax DefaultLiteralExpression { get; } = SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression); + + public static LiteralExpressionSyntax FalseLiteralExpression { get; } = SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression); + + public static LiteralExpressionSyntax NullLiteralExpression { get; } = SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression); + + public static LiteralExpressionSyntax TrueLiteralExpression { get; } = SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression); + + public static LiteralExpressionSyntax LiteralExpression(bool value) + { + return value ? TrueLiteralExpression : FalseLiteralExpression; + } + + public static LiteralExpressionSyntax NumericLiteralExpression(int value) + { + return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value)); + } + + public static LiteralExpressionSyntax NumericLiteralExpression(SyntaxToken literal) + { + return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, literal); + } + + public static LiteralExpressionSyntax StringLiteralExpression(string value) + { + return SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(value)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.OperatorDeclaration.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.OperatorDeclaration.cs new file mode 100644 index 0000000..d32a6b2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.OperatorDeclaration.cs @@ -0,0 +1,40 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static partial class FastSyntaxFactory +{ + public static OperatorDeclarationSyntax EqualsEqualsOperatorDeclaration(TypeSyntax returnType) + { + return SyntaxFactory.OperatorDeclaration(returnType, SyntaxFactory.Token(SyntaxKind.EqualsEqualsToken)); + } + + public static OperatorDeclarationSyntax ExclamationEqualsOperatorDeclaration(TypeSyntax returnType) + { + return SyntaxFactory.OperatorDeclaration(returnType, SyntaxFactory.Token(SyntaxKind.ExclamationEqualsToken)); + } + + public static OperatorDeclarationSyntax MinusOperatorDeclaration(TypeSyntax returnType) + { + return SyntaxFactory.OperatorDeclaration(returnType, SyntaxFactory.Token(SyntaxKind.MinusToken)); + } + + public static OperatorDeclarationSyntax MinusMinusOperatorDeclaration(TypeSyntax returnType) + { + return SyntaxFactory.OperatorDeclaration(returnType, SyntaxFactory.Token(SyntaxKind.MinusMinusToken)); + } + + public static OperatorDeclarationSyntax PlusOperatorDeclaration(TypeSyntax returnType) + { + return SyntaxFactory.OperatorDeclaration(returnType, SyntaxFactory.Token(SyntaxKind.PlusToken)); + } + + public static OperatorDeclarationSyntax PlusPlusOperatorDeclaration(TypeSyntax returnType) + { + return SyntaxFactory.OperatorDeclaration(returnType, SyntaxFactory.Token(SyntaxKind.PlusPlusToken)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.PredefinedType.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.PredefinedType.cs new file mode 100644 index 0000000..00d6f8f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.PredefinedType.cs @@ -0,0 +1,27 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static partial class FastSyntaxFactory +{ + public static NullableTypeSyntax NullableObjectType { get; } = SyntaxFactory.NullableType(SyntaxFactory.PredefinedType(ObjectKeyword)); + + public static NullableTypeSyntax NullableStringType { get; } = SyntaxFactory.NullableType(SyntaxFactory.PredefinedType(StringKeyword)); + + public static PredefinedTypeSyntax BoolType { get; } = SyntaxFactory.PredefinedType(BoolKeyword); + + public static PredefinedTypeSyntax IntType { get; } = SyntaxFactory.PredefinedType(IntKeyword); + + public static PredefinedTypeSyntax ObjectType { get; } = SyntaxFactory.PredefinedType(ObjectKeyword); + + public static PredefinedTypeSyntax StringType { get; } = SyntaxFactory.PredefinedType(StringKeyword); + + public static PredefinedTypeSyntax UIntType { get; } = SyntaxFactory.PredefinedType(UIntKeyword); + + public static PredefinedTypeSyntax VoidType { get; } = SyntaxFactory.PredefinedType(VoidKeyword); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.TokenList.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.TokenList.cs new file mode 100644 index 0000000..9818828 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.TokenList.cs @@ -0,0 +1,64 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static partial class FastSyntaxFactory +{ + public static SyntaxTokenList InternalTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword); + + public static SyntaxTokenList InternalAbstractTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword, AbstractKeyword); + + public static SyntaxTokenList InternalAbstractPartialTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword, AbstractKeyword, PartialKeyword); + + public static SyntaxTokenList InternalReadOnlyPartialTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword, ReadOnlyKeyword, PartialKeyword); + + public static SyntaxTokenList InternalSealedTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword, SealedKeyword); + + public static SyntaxTokenList InternalStaticTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword, StaticKeyword); + + public static SyntaxTokenList InternalStaticPartialTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword, StaticKeyword, PartialKeyword); + + public static SyntaxTokenList InternalPartialTokenList { get; } = SyntaxFactory.TokenList(InternalKeyword, PartialKeyword); + + public static SyntaxTokenList ParamsTokenList { get; } = SyntaxFactory.TokenList(ParamsKeyword); + + public static SyntaxTokenList PartialTokenList { get; } = SyntaxFactory.TokenList(PartialKeyword); + + public static SyntaxTokenList PrivateTokenList { get; } = SyntaxFactory.TokenList(PrivateKeyword); + + public static SyntaxTokenList PrivatePartialTokenList { get; } = SyntaxFactory.TokenList(PrivateKeyword, PartialKeyword); + + public static SyntaxTokenList PrivateProtectedTokenList { get; } = SyntaxFactory.TokenList(PrivateKeyword, ProtectedKeyword); + + public static SyntaxTokenList PrivateStaticExternTokenList { get; } = SyntaxFactory.TokenList(PrivateKeyword, StaticKeyword, ExternKeyword); + + public static SyntaxTokenList PrivateStaticReadonlyTokenList { get; } = SyntaxFactory.TokenList(PrivateKeyword, StaticKeyword, ReadOnlyKeyword); + + public static SyntaxTokenList ProtectedTokenList { get; } = SyntaxFactory.TokenList(ProtectedKeyword); + + public static SyntaxTokenList ProtectedInternalTokenList { get; } = SyntaxFactory.TokenList(ProtectedKeyword, InternalKeyword); + + public static SyntaxTokenList PublicTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword); + + public static SyntaxTokenList PublicConstTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword, ConstKeyword); + + public static SyntaxTokenList PublicOverrideTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword, OverrideKeyword); + + public static SyntaxTokenList PublicPartialTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword, PartialKeyword); + + public static SyntaxTokenList PublicReadOnlyTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword, ReadOnlyKeyword); + + public static SyntaxTokenList PublicStaticTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword, StaticKeyword); + + public static SyntaxTokenList PublicStaticPartialTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword, StaticKeyword, PartialKeyword); + + public static SyntaxTokenList PublicStaticUnsafeTokenList { get; } = SyntaxFactory.TokenList(PublicKeyword, StaticKeyword, UnsafeKeyword); + + public static SyntaxTokenList StaticTokenList { get; } = SyntaxFactory.TokenList(StaticKeyword); + + public static SyntaxTokenList ThisTokenList { get; } = SyntaxFactory.TokenList(ThisKeyword); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.UnaryExpression.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.UnaryExpression.cs new file mode 100644 index 0000000..651a2a6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.UnaryExpression.cs @@ -0,0 +1,39 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static partial class FastSyntaxFactory +{ + public static PostfixUnaryExpressionSyntax SuppressNullableWarningExpression(ExpressionSyntax operand) + { + return SyntaxFactory.PostfixUnaryExpression(SyntaxKind.SuppressNullableWarningExpression, operand); + } + + public static PrefixUnaryExpressionSyntax AddressOfExpression(ExpressionSyntax operand) + { + return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.AddressOfExpression, operand); + } + + public static PrefixUnaryExpressionSyntax LogicalNotExpression(ExpressionSyntax operand) + { + return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, operand); + } + + public static PrefixUnaryExpressionSyntax PointerIndirectionExpression(ExpressionSyntax operand) + { + return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression, operand); + } + + public static PrefixUnaryExpressionSyntax PreDecrementExpression(ExpressionSyntax operand) + { + return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.PreDecrementExpression, operand); + } + + public static PrefixUnaryExpressionSyntax PreIncrementExpression(ExpressionSyntax operand) + { + return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.PreIncrementExpression, operand); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.cs new file mode 100644 index 0000000..2e63daf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/FastSyntaxFactory.cs @@ -0,0 +1,134 @@ +// 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 System; +using System.Collections.Immutable; +using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +// Properties and methods are ordered by return type +internal static partial class FastSyntaxFactory +{ + public static SyntaxToken SemicolonToken { get; } = SyntaxFactory.Token(SyntaxKind.SemicolonToken); + + public static ArgumentListSyntax EmptyArgumentList { get; } = SyntaxFactory.ArgumentList(); + + public static TypeArgumentListSyntax EmptyTypeArgumentList { get; } = SyntaxFactory.TypeArgumentList(); + + public static BlockSyntax EmptyBlock { get; } = SyntaxFactory.Block(); + + public static SyntaxTriviaList NullableEnableTriviaList { get; } = SyntaxFactory.TriviaList(SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(EnableKeyword, true))); + + public static AccessorListSyntax GetAndSetAccessorList { get; } = SyntaxFactory.AccessorList(SyntaxFactory.List( + [ + GetAccessorDeclaration(), + SetAccessorDeclaration() + ])); + + public static AccessorDeclarationSyntax GetAccessorDeclaration() + { + return SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SemicolonToken); + } + + public static AccessorDeclarationSyntax SetAccessorDeclaration() + { + return SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SemicolonToken); + } + + public static AssignmentExpressionSyntax CoalesceAssignmentExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.AssignmentExpression(SyntaxKind.CoalesceAssignmentExpression, left, right); + } + + public static AssignmentExpressionSyntax SimpleAssignmentExpression(ExpressionSyntax left, ExpressionSyntax right) + { + return SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, left, right); + } + + public static ConstructorDeclarationSyntax WithEmptyBlockBody(this ConstructorDeclarationSyntax constructor) + { + return constructor.WithBody(EmptyBlock); + } + + public static ConstructorInitializerSyntax BaseConstructorInitializer(ArgumentListSyntax? argumentList = null) + { + return SyntaxFactory.ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, argumentList); + } + + public static ConversionOperatorDeclarationSyntax ImplicitConversionOperatorDeclaration(TypeSyntax type) + { + return SyntaxFactory.ConversionOperatorDeclaration(ImplicitKeyword, type); + } + + public static FileScopedNamespaceDeclarationSyntax FileScopedNamespaceDeclaration(string qualifiedName) + { + return SyntaxFactory.FileScopedNamespaceDeclaration(SyntaxFactory.ParseName(qualifiedName)); + } + + public static FileScopedNamespaceDeclarationSyntax FileScopedNamespaceDeclaration(INamespaceSymbol symbol) + { + return SyntaxFactory.FileScopedNamespaceDeclaration(SyntaxFactory.ParseName(symbol.ToDisplayString())); + } + + public static InvocationExpressionSyntax WithEmptyArgumentList(this InvocationExpressionSyntax expression) + { + return expression.WithArgumentList(EmptyArgumentList); + } + + public static InvocationExpressionSyntax NameOfExpression(ExpressionSyntax argument) + { + return SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("nameof"), SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(argument)))); + } + + public static MemberAccessExpressionSyntax SimpleMemberAccessExpression(ExpressionSyntax expression, SimpleNameSyntax name) + { + return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expression, name); + } + + public static ObjectCreationExpressionSyntax WithEmptyArgumentList(this ObjectCreationExpressionSyntax expression) + { + return expression.WithArgumentList(EmptyArgumentList); + } + + public static ParameterSyntax Parameter(TypeSyntax type, SyntaxToken name) + { + return SyntaxFactory.Parameter(name).WithType(type); + } + + public static TypeDeclarationSyntax PartialTypeDeclaration(INamedTypeSymbol typeSymbol) + { + string typeName = typeSymbol.Name; + + TypeDeclarationSyntax typeDeclaration = (typeSymbol.TypeKind, typeSymbol.IsRecord) switch + { + (TypeKind.Class, false) => SyntaxFactory.ClassDeclaration(typeName), + (TypeKind.Class, true) => SyntaxFactory.RecordDeclaration(SyntaxKind.RecordDeclaration, RecordKeyword, typeName) + .WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)), + (TypeKind.Struct, false) => SyntaxFactory.StructDeclaration(typeName), + (TypeKind.Struct, true) => SyntaxFactory.RecordDeclaration(SyntaxKind.RecordStructDeclaration, RecordKeyword, typeName) + .WithClassOrStructKeyword(StructKeyword) + .WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)), + (TypeKind.Interface, _) => SyntaxFactory.InterfaceDeclaration(typeName), + _ => throw new InvalidOperationException("Unsupported type kind for partial declaration: " + typeSymbol.TypeKind) + }; + + if (typeSymbol.IsGenericType) + { + typeDeclaration = typeDeclaration.WithTypeParameterList(SyntaxFactory.TypeParameterList(SyntaxFactory.SeparatedList( + ImmutableArray.CreateRange(typeSymbol.TypeParameters, static tParam => SyntaxFactory.TypeParameter(tParam.Name))))); + } + + return typeDeclaration.WithModifiers(PartialTokenList); + } + + public static UsingDirectiveSyntax UsingDirective(string qualifiedName) + { + return SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(qualifiedName)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/ImmutableArrayBuilder.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/ImmutableArrayBuilder.cs new file mode 100644 index 0000000..f17bd38 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/ImmutableArrayBuilder.cs @@ -0,0 +1,194 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal ref struct ImmutableArrayBuilder : IDisposable +{ + private Writer? writer; + + public static ImmutableArrayBuilder Rent() + { + return new(new()); + } + + private ImmutableArrayBuilder(Writer writer) + { + this.writer = writer; + } + + public readonly int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => writer!.Count; + } + + [UnscopedRef] + public readonly ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => writer!.WrittenSpan; + } + + public readonly void Add(T item) + { + writer!.Add(item); + } + + public readonly void AddRange(scoped ReadOnlySpan items) + { + writer!.AddRange(items); + } + + public readonly ImmutableArray ToImmutable() + { + T[] array = writer!.WrittenSpan.ToArray(); + + return Unsafe.As>(ref array); + } + + public readonly T[] ToArray() + { + return writer!.WrittenSpan.ToArray(); + } + + public readonly IEnumerable AsEnumerable() + { + return writer!; + } + + public override readonly string ToString() + { + return writer!.WrittenSpan.ToString(); + } + + public void Dispose() + { + Writer? writer = this.writer; + + this.writer = null; + + writer?.Dispose(); + } + + private sealed class Writer : ICollection, IDisposable + { + private T?[]? array; + private int index; + + public Writer() + { + array = ArrayPool.Shared.Rent(typeof(T) == typeof(char) ? 1024 : 8); + index = 0; + } + + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => index; + } + + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(array!, 0, index); + } + + bool ICollection.IsReadOnly { get => true; } + + public void Add(T value) + { + EnsureCapacity(1); + array![index++] = value; + } + + public void AddRange(ReadOnlySpan items) + { + EnsureCapacity(items.Length); + + items.CopyTo(array.AsSpan(index)!); + + index += items.Length; + } + + public void Dispose() + { + T?[]? array = this.array; + + this.array = null; + + if (array is not null) + { + ArrayPool.Shared.Return(array, clearArray: typeof(T) != typeof(char)); + } + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection.Contains(T item) + { + throw new NotSupportedException(); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(this.array!, 0, array, arrayIndex, this.index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + T?[] array = this.array!; + int length = this.index; + + for (int i = 0; i < length; i++) + { + yield return array[i]!; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + bool ICollection.Remove(T item) + { + throw new NotSupportedException(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int requestedSize) + { + if (requestedSize > array!.Length - index) + { + ResizeBuffer(requestedSize); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer(int sizeHint) + { + int minimumSize = index + sizeHint; + + T?[] oldArray = array!; + T?[] newArray = ArrayPool.Shared.Rent(minimumSize); + + Array.Copy(oldArray, newArray, index); + + array = newArray; + + ArrayPool.Shared.Return(oldArray, clearArray: typeof(T) != typeof(char)); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SymbolDisplayFormats.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SymbolDisplayFormats.cs new file mode 100644 index 0000000..692944c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SymbolDisplayFormats.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static class SymbolDisplayFormats +{ + public static SymbolDisplayFormat NullableFullyQualifiedFormat { get; } = SymbolDisplayFormat.FullyQualifiedFormat + .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + public static SymbolDisplayFormat FullyQualifiedFormatWithoutTypeParameters { get; } = + new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SyntaxKeywords.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SyntaxKeywords.cs new file mode 100644 index 0000000..e0e5860 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SyntaxKeywords.cs @@ -0,0 +1,64 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static class SyntaxKeywords +{ + public static SyntaxToken AbstractKeyword { get; } = SyntaxFactory.Token(SyntaxKind.AbstractKeyword); + + public static SyntaxToken BoolKeyword { get; } = SyntaxFactory.Token(SyntaxKind.BoolKeyword); + + public static SyntaxToken ClassKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ClassKeyword); + + public static SyntaxToken ConstKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ConstKeyword); + + public static SyntaxToken EnableKeyword { get; } = SyntaxFactory.Token(SyntaxKind.EnableKeyword); + + public static SyntaxToken ExternKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ExternKeyword); + + public static SyntaxToken FieldKeyword { get; } = SyntaxFactory.Token(SyntaxKind.FieldKeyword); + + public static SyntaxToken ImplicitKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ImplicitKeyword); + + public static SyntaxToken IntKeyword { get; } = SyntaxFactory.Token(SyntaxKind.IntKeyword); + + public static SyntaxToken InternalKeyword { get; } = SyntaxFactory.Token(SyntaxKind.InternalKeyword); + + public static SyntaxToken ObjectKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ObjectKeyword); + + public static SyntaxToken OverrideKeyword { get; } = SyntaxFactory.Token(SyntaxKind.OverrideKeyword); + + public static SyntaxToken ParamsKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ParamsKeyword); + + public static SyntaxToken PartialKeyword { get; } = SyntaxFactory.Token(SyntaxKind.PartialKeyword); + + public static SyntaxToken PrivateKeyword { get; } = SyntaxFactory.Token(SyntaxKind.PrivateKeyword); + + public static SyntaxToken ProtectedKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ProtectedKeyword); + + public static SyntaxToken PublicKeyword { get; } = SyntaxFactory.Token(SyntaxKind.PublicKeyword); + + public static SyntaxToken ReadOnlyKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword); + + public static SyntaxToken RecordKeyword { get; } = SyntaxFactory.Token(SyntaxKind.RecordKeyword); + + public static SyntaxToken SealedKeyword { get; } = SyntaxFactory.Token(SyntaxKind.SealedKeyword); + + public static SyntaxToken StaticKeyword { get; } = SyntaxFactory.Token(SyntaxKind.StaticKeyword); + + public static SyntaxToken StringKeyword { get; } = SyntaxFactory.Token(SyntaxKind.StringKeyword); + + public static SyntaxToken StructKeyword { get; } = SyntaxFactory.Token(SyntaxKind.StructKeyword); + + public static SyntaxToken ThisKeyword { get; } = SyntaxFactory.Token(SyntaxKind.ThisKeyword); + + public static SyntaxToken UIntKeyword { get; } = SyntaxFactory.Token(SyntaxKind.UIntKeyword); + + public static SyntaxToken UnsafeKeyword { get; } = SyntaxFactory.Token(SyntaxKind.UnsafeKeyword); + + public static SyntaxToken VoidKeyword { get; } = SyntaxFactory.Token(SyntaxKind.VoidKeyword); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SyntaxNodeHelper.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SyntaxNodeHelper.cs new file mode 100644 index 0000000..b27ac4d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Primitive/SyntaxNodeHelper.cs @@ -0,0 +1,32 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Threading; + +namespace Snap.Hutao.SourceGeneration.Primitive; + +internal static class SyntaxNodeHelper +{ + public static bool Is(SyntaxNode node, CancellationToken token) + where T : SyntaxNode + { + token.ThrowIfCancellationRequested(); + return node is T; + } + + public static bool Is(SyntaxNode node, CancellationToken token) + where T1 : SyntaxNode + where T2 : SyntaxNode + { + token.ThrowIfCancellationRequested(); + return node is (T1 or T2); + } + + public static bool TypeHasBaseType(SyntaxNode node, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + return node is TypeDeclarationSyntax { BaseList: not null }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/CompositeFormat.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/CompositeFormat.cs new file mode 100644 index 0000000..3a63d1f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/CompositeFormat.cs @@ -0,0 +1,358 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.SourceGeneration.Resx; + +/// Represents a parsed composite format string. +[DebuggerDisplay("{Format}")] +public sealed class CompositeFormat +{ + /// The parsed segments that make up the composite format string. + /// + /// Every segment represents either a literal or a format hole, based on whether Literal + /// is non-null or ArgIndex is non-negative. + /// + private readonly (string? Literal, int ArgIndex, int Alignment, string? Format)[] segments; + + /// The sum of the lengths of all of the literals in . + private readonly int literalLength; + + /// The number of segments in that represent format holes. + private readonly int formattedCount; + + /// The number of args required to satisfy the format holes. + /// This is equal to one more than the largest index required by any format hole. + private readonly int argsRequired; + + /// Initializes the instance. + /// The composite format string that was parsed. + /// The parsed segments. + private CompositeFormat(string format, (string? Literal, int ArgIndex, int Alignment, string? Format)[] segments) + { + // Store the format. + Format = format; + + // Store the segments. + this.segments = segments; + + // Compute derivative information from the segments. + int literalLength = 0, formattedCount = 0, argsRequired = 0; + foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in segments) + { + Debug.Assert((segment.Literal is not null) ^ (segment.ArgIndex >= 0), "The segment should represent a literal or a format hole, but not both."); + + if (segment.Literal is { } literal) + { + literalLength += literal.Length; // no concern about overflow as these were parsed out of a single string + } + else if (segment.ArgIndex >= 0) + { + formattedCount++; + argsRequired = Math.Max(argsRequired, segment.ArgIndex + 1); + } + } + + // Store the derivative information. + Debug.Assert(literalLength >= 0); + Debug.Assert(formattedCount >= 0); + Debug.Assert(formattedCount == 0 || argsRequired > 0); + this.literalLength = literalLength; + this.formattedCount = formattedCount; + this.argsRequired = argsRequired; + } + + /// Parse the composite format string . + /// The string to parse. + /// The parsed . + /// is null. + /// A format item in is invalid. + public static CompositeFormat Parse([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format) + { + if (format is null) + { + throw new ArgumentNullException(nameof(format)); + } + + List<(string? Literal, int ArgIndex, int Alignment, string? Format)> segments = []; + if (!TryParseLiterals(format, segments)) + { + throw new FormatException(); + } + + return new(format, segments.ToArray()); + } + + public static bool TryParse([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string? format, [NotNullWhen(true)] out CompositeFormat? compositeFormat) + { + if (format is null) + { + compositeFormat = null; + return false; + } + + List<(string? Literal, int ArgIndex, int Alignment, string? Format)> segments = []; + if (!TryParseLiterals(format, segments)) + { + compositeFormat = null; + return false; + } + + compositeFormat = new(format, segments.ToArray()); + return true; + } + + /// Gets the original composite format string used to create this instance. + public string Format { get; } + + /// Gets the minimum number of arguments that must be passed to a formatting operation using this . + /// It's permissible to supply more arguments than this value, but it's an error to pass fewer. + public int MinimumArgumentCount { get => argsRequired; } + + /// Throws an exception if the specified number of arguments is fewer than the number required. + /// The number of arguments provided by the caller. + /// An insufficient number of arguments were provided. + internal void ValidateNumberOfArgs(int numArgs) + { + if (numArgs < argsRequired) + { + throw new FormatException(); + } + } + + /// Parse the composite format string into segments. + /// The format string. + /// The list into which to store the segments. + /// true if the format string can be parsed successfully; otherwise, false. + private static bool TryParseLiterals(ReadOnlySpan format, List<(string? Literal, int ArgIndex, int Alignment, string? Format)> segments) + { + // This parsing logic is copied from string.Format. It's the same code modified to not format + // as part of parsing and instead store the parsed literals and argument specifiers (alignment + // and format) for later use. + + // Rather than parsing directly into the segments list, literals are parsed into a reusable builder. + // Due to the nature of the parsing logic copied from string.Format, and our desire not to veer from + // it significantly in order to maintain compatibility and accidental regression, multiple literals + // next to each other might be parsed separately due to braces in between them. This builder then + // allows us to merge those segments back together easily prior to their being appended to the list. + ValueStringBuilder vsb = new(stackalloc char[ /*string.StackallocCharBufferSizeLimit*/256]); + + // Repeatedly find the next hole and process it. + int pos = 0; + char ch; + while (true) + { + // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. + // Along the way we need to also unescape escaped closing braces. + while (true) + { + // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. + ReadOnlySpan remainder = format[pos..]; + int countUntilNextBrace = remainder.IndexOfAny('{', '}'); + if (countUntilNextBrace < 0) + { + vsb.Append(remainder); + segments.Add((vsb.ToString(), -1, 0, null)); + return true; + } + + // Append the text until the brace. + vsb.Append(remainder[..countUntilNextBrace]); + pos += countUntilNextBrace; + + // Get the brace. It must be followed by another character, either a copy of itself in the case of being + // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. + char brace = format[pos]; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + if (brace == ch) + { + vsb.Append(ch); + pos++; + continue; + } + + // This wasn't an escape, so it must be an opening brace. + if (brace != '{') + { + return false; + } + + // Proceed to parse the hole. + segments.Add((vsb.ToString(), -1, 0, null)); + vsb.Length = 0; + break; + } + + // We're now positioned just after the opening brace of an argument hole, which consists of + // an opening brace, an index, an optional width preceded by a comma, and an optional format + // preceded by a colon, with arbitrary amounts of spaces throughout. + int width = 0; + string? itemFormat = null; // used if itemFormat is null + + // First up is the index parameter, which is of the form: + // at least on digit + // optional any number of spaces + // We've already read the first digit into ch. + Debug.Assert(format[pos - 1] == '{'); + Debug.Assert(ch != '{'); + int index = ch - '0'; + if ((uint)index >= 10u) + { + return false; + } + + // Common case is a single digit index followed by a closing brace. If it's not a closing brace, + // proceed to finish parsing the full hole format. + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + if (ch != '}') + { + // Continue consuming optional additional digits. + while ((uint)(ch - '0') <= ('9' - '0')) + { + index = index * 10 + ch - '0'; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + + // Consume optional whitespace. + while (ch == ' ') + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + + // Parse the optional alignment, which is of the form: + // comma + // optional any number of spaces + // optional - + // at least one digit + // optional any number of spaces + if (ch == ',') + { + // Consume optional whitespace. + do + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + while (ch == ' '); + + // Consume an optional minus sign indicating left alignment. + int leftJustify = 1; + if (ch == '-') + { + leftJustify = -1; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + + // Parse alignment digits. The read character must be a digit. + width = ch - '0'; + if ((uint)width >= 10u) + { + return false; + } + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + while ((uint)(ch - '0') <= ('9' - '0')) + { + width = width * 10 + ch - '0'; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + width *= leftJustify; + + // Consume optional whitespace + while (ch == ' ') + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + } + + // The next character needs to either be a closing brace for the end of the hole, + // or a colon indicating the start of the format. + if (ch != '}') + { + if (ch != ':') + { + // Unexpected character + return false; + } + + // Search for the closing brace; everything in between is the format, + // but opening braces aren't allowed. + int startingPos = pos; + while (true) + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + + if (ch == '}') + { + // Argument hole closed + break; + } + + if (ch == '{') + { + // Braces inside the argument hole are not supported + return false; + } + } + + startingPos++; + itemFormat = format.Slice(startingPos, pos - startingPos).ToString(); + } + } + + Debug.Assert(format[pos] == '}'); + pos++; + + segments.Add((null, index, width, itemFormat)); + + // Continue parsing the rest of the format string. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TryMoveNext(ReadOnlySpan format, ref int pos, out char nextChar) + { + pos++; + if ((uint)pos >= (uint)format.Length) + { + nextChar = '\0'; + return false; + } + + nextChar = format[pos]; + return true; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/ResxGenerator2.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/ResxGenerator2.cs new file mode 100644 index 0000000..7b5a837 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/ResxGenerator2.cs @@ -0,0 +1,861 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Snap.Hutao.SourceGeneration.Extension; +using Snap.Hutao.SourceGeneration.Model; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +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.Resx; + +[Generator] +public sealed class ResxGenerator2 : IIncrementalGenerator +{ + private static readonly DiagnosticDescriptor InvalidResx = new("SH401", "Couldn't parse Resx file", "Couldn't parse Resx file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true); + private static readonly DiagnosticDescriptor InvalidPropertiesForNamespace = new("SH402", "Couldn't compute namespace", "Couldn't compute namespace for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true); + private static readonly DiagnosticDescriptor InvalidPropertiesForResourceName = new("SH403", "Couldn't compute resource name", "Couldn't compute resource name for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true); + private static readonly DiagnosticDescriptor InconsistentProperties = new("SH404", "Inconsistent properties", "Property '{0}' values for '{1}' are inconsistent", "ResxGenerator", DiagnosticSeverity.Warning, true); + private static readonly DiagnosticDescriptor NameShouldNotEndsWithFormat = new("SH405", "Resource data name should not ends with 'Format'", "Resource data '{0}' should not ends with 'Format'", "ResxGenerator", DiagnosticSeverity.Warning, true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValueProvider assemblyNameProvider = context.CompilationProvider + .Select(static (compilation, token) => compilation.AssemblyName); + + IncrementalValuesProvider resxProvider = context.AdditionalTextsProvider + .Where(static text => text.Path.EndsWith(".resx", StringComparison.OrdinalIgnoreCase)) + .Combine(context.AnalyzerConfigOptionsProvider.Combine(assemblyNameProvider)) + .Select(static (tuple, token) => ResxFile.Create(tuple.Left, tuple.Right.Left, tuple.Right.Right, token)) + .Where(static file => file is { ResourceName: not null, Namespace: not null, ClassName: not null }) + .GroupBy(static file => (file.Namespace!, file.ClassName!, file.ResourceName!)) + .Select(ResxGeneratorContext.Create); + + context.RegisterSourceOutput(resxProvider, GenerateWrapper); + } + + private static void GenerateWrapper(SourceProductionContext production, ResxGeneratorContext context) + { + try + { + Generate(production, context); + } + catch (Exception e) + { + production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString()); + } + } + + private static void Generate(SourceProductionContext production, ResxGeneratorContext context) + { + if (context.Entries.IsEmpty) + { + return; + } + + if (context.Namespace is null) + { + production.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForNamespace, Location.None, context.ResourceName)); + return; + } + + if (context.ResourceName is null) + { + production.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForResourceName, Location.None, context.ResourceName)); + return; + } + + CompilationUnitSyntax standard = GenerateStandardCompilationUnit(production, context); + production.AddSource($"{context.ResourceName}.cs", standard.ToFullStringWithHeader()); + + CompilationUnitSyntax nameEnum = GenerateNameEnumCompilationUnit(context); + production.AddSource($"{context.ResourceName}Name.cs", nameEnum.ToFullStringWithHeader()); + } + + private static CompilationUnitSyntax GenerateStandardCompilationUnit(SourceProductionContext production, ResxGeneratorContext context) + { + return CompilationUnit() + .WithMembers(SingletonList(FileScopedNamespaceDeclaration(context.Namespace!) + .WithMembers(SingletonList( + ClassDeclaration(context.ClassName!) + .WithModifiers(InternalAbstractPartialTokenList) + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(List( + [ + .. GenerateSharedMemberDeclarations(context), + .. GenerateEntryMemberDeclarations(production, context) + ])))))) + .NormalizeWhitespace(); + } + + private static IEnumerable GenerateSharedMemberDeclarations(ResxGeneratorContext context) + { + // [field: global::System.Diagnostics.CodeAnalysis.MaybeNull] + // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Advanced)] + // public static global::System.Resources.ResourceManager ResourceManager + // { + // get => field ??= new("${namespace}", typeof(${className}).Assembly); + // } + yield return PropertyDeclaration(TypeOfSystemResourcesResourceManager, Identifier("ResourceManager")) + .WithAttributeLists(List( + [ + AttributeList(SingletonSeparatedList(Attribute(NameOfSystemDiagnosticsCodeAnalysisMaybeNull))) + .WithTarget(AttributeTargetSpecifier(FieldKeyword)), + AttributeList(SingletonSeparatedList(Attribute(NameOfSystemComponentModelEditorBrowsable) + .WithArgumentList(AttributeArgumentList(SingletonSeparatedList( + AttributeArgument(SimpleMemberAccessExpression( + NameOfSystemComponentModelEditorBrowsableState, + IdentifierName("Advanced")))))))) + ])) + .WithModifiers(PublicStaticTokenList) + .WithAccessorList(AccessorList(SingletonList( + GetAccessorDeclaration() + .WithExpressionBody(ArrowExpressionClause(CoalesceAssignmentExpression( + FieldExpression(), + ImplicitObjectCreationExpression() + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(StringLiteralExpression(context.ResourceName!)), + Argument(SimpleMemberAccessExpression( + TypeOfExpression(IdentifierName(context.ClassName!)), + IdentifierName("Assembly"))), + ]))))))))); + + // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Advanced)] + // public static global::System.Globalization.CultureInfo? Culture { get; set; } + yield return PropertyDeclaration(NullableType(TypeOfSystemGlobalizationCultureInfo), Identifier("Culture")) + .WithAttributeLists(SingletonList(AttributeList(SingletonSeparatedList( + Attribute(NameOfSystemComponentModelEditorBrowsable) + .WithArgumentList(AttributeArgumentList(SingletonSeparatedList( + AttributeArgument(SimpleMemberAccessExpression( + NameOfSystemComponentModelEditorBrowsableState, + IdentifierName("Advanced")))))))))) + .WithModifiers(PublicStaticTokenList) + .WithAccessorList(GetAndSetAccessorList); + + // [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(defaultValue))] + // public static object? GetObject(string name, global::System.Globalization.CultureInfo? culture, object? defaultValue) + // { + // return ResourceManager.GetObject(name, culture ?? Culture) ?? defaultValue; + // } + yield return MethodDeclaration(NullableObjectType, Identifier("GetObject")) + .WithAttributeLists(SingletonList( + ReturnNotNullIfNotNullAttributeList("defaultValue"))) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(StringType, Identifier("name")), + Parameter(NullableType(TypeOfSystemGlobalizationCultureInfo), Identifier("culture")), + Parameter(NullableObjectType, Identifier("defaultValue")) + ]))) + .WithBody(Block(SingletonList( + ReturnStatement(CoalesceExpression( + InvocationExpression(SimpleMemberAccessExpression( + IdentifierName("ResourceManager"), + IdentifierName("GetObject"))) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(CoalesceExpression(IdentifierName("culture"), IdentifierName("Culture"))), + ]))), + IdentifierName("defaultValue")))))); + + // public static object? GetObject(string name, global::System.Globalization.CultureInfo? culture) + // { + // return GetObject(name, culture,default); + // } + yield return MethodDeclaration(NullableObjectType, Identifier("GetObject")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(StringType, Identifier("name")), + Parameter(NullableType(TypeOfSystemGlobalizationCultureInfo), Identifier("culture")) + ]))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(IdentifierName("GetObject")) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(IdentifierName("culture")), + Argument(DefaultLiteralExpression) + ]))))))); + + // public static object? GetObject(string name) + // { + // return GetObject(name, default, default); + // } + yield return MethodDeclaration(NullableObjectType, Identifier("GetObject")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(StringType, Identifier("name"))))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(IdentifierName("GetObject")) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(DefaultLiteralExpression), + Argument(DefaultLiteralExpression), + ]))))))); + + // [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(defaultValue))] + // public static object? GetObject(string name, object? defaultValue) + // { + // return GetObject(name, default, defaultValue); + // } + yield return MethodDeclaration(NullableObjectType, Identifier("GetObject")) + .WithAttributeLists(SingletonList( + ReturnNotNullIfNotNullAttributeList("defaultValue"))) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(StringType, Identifier("name")), + Parameter(NullableObjectType, Identifier("defaultValue")) + ]))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(IdentifierName("GetObject")) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(DefaultLiteralExpression), + Argument(IdentifierName("defaultValue")), + ]))))))); + + // public static global::System.IO.Stream? GetStream(string name, global::System.Globalization.CultureInfo? culture) + // { + // return ResourceManager.GetStream(name, culture ?? Culture); + // } + yield return MethodDeclaration(NullableType(TypeOfSystemIOStream), Identifier("GetStream")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(StringType, Identifier("name")), + Parameter(NullableType(TypeOfSystemGlobalizationCultureInfo), Identifier("culture")) + ]))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(SimpleMemberAccessExpression( + IdentifierName("ResourceManager"), + IdentifierName("GetStream"))) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(CoalesceExpression(IdentifierName("culture"), IdentifierName("Culture"))), + ]))))))); + + // public static global::System.IO.Stream? GetStream(string name) + // { + // return GetStream(name, default); + // } + yield return MethodDeclaration(NullableType(TypeOfSystemIOStream), Identifier("GetStream")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(StringType, Identifier("name"))))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(IdentifierName("GetStream")) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(DefaultLiteralExpression), + ]))))))); + + // public static string? GetString(string name, global::System.Globalization.CultureInfo? culture, params object?[]? args) + // { + // culture ??= Culture; + // string? str = ResourceManager.GetString(name, culture); + // if (str is null) + // { + // return default; + // } + // + // if (args is null) + // { + // return str; + // } + // + // return string.Format(culture, str, args); + // } + yield return MethodDeclaration(NullableStringType, Identifier("GetString")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(StringType, Identifier("name")), + Parameter(NullableType(TypeOfSystemGlobalizationCultureInfo), Identifier("culture")), + NullableParamsArrayOfNullableObjectTypeParameter("args") + ]))) + .WithBody(Block(List( + [ + ExpressionStatement(CoalesceAssignmentExpression( + IdentifierName("culture"), + IdentifierName("Culture"))), + LocalDeclarationStatement(VariableDeclaration(NullableStringType) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(Identifier("str")) + .WithInitializer(EqualsValueClause( + InvocationExpression(SimpleMemberAccessExpression( + IdentifierName("ResourceManager"), + IdentifierName("GetString"))) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(IdentifierName("culture")), + ])))))))), + IfStatement( + IsPatternExpression(IdentifierName("str"), ConstantPattern(NullLiteralExpression)), + Block(SingletonList(ReturnStatement(DefaultLiteralExpression)))), + IfStatement( + IsPatternExpression(IdentifierName("args"), ConstantPattern(NullLiteralExpression)), + Block(SingletonList(ReturnStatement(IdentifierName("str"))))), + ReturnStatement( + InvocationExpression(SimpleMemberAccessExpression( + StringType, + IdentifierName("Format"))) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("culture")), + Argument(IdentifierName("str")), + Argument(IdentifierName("args")) + ])))) + ]))); + + // public static string? GetString(string name, params object?[]? args) + // { + // return GetString(name, null, args); + // } + yield return MethodDeclaration(NullableStringType, Identifier("GetString")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(StringType, Identifier("name")), + NullableParamsArrayOfNullableObjectTypeParameter("args") + ]))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(IdentifierName("GetString")) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(DefaultLiteralExpression), + Argument(IdentifierName("args")) + ]))))))); + + // public static string? GetString(string name, global::System.Globalization.CultureInfo? culture) + // { + // return ResourceManager.GetString(name, culture ?? Culture); + // } + yield return MethodDeclaration(NullableStringType, Identifier("GetString")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(StringType, Identifier("name")), + Parameter(NullableType(TypeOfSystemGlobalizationCultureInfo), Identifier("culture")) + ]))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(SimpleMemberAccessExpression( + IdentifierName("ResourceManager"), + IdentifierName("GetString"))) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(CoalesceExpression( + IdentifierName("culture"), + IdentifierName("Culture"))), + ]))))))); + + // public static string? GetString(string name) + // { + // return ResourceManager.GetString(name, Culture); + // } + yield return MethodDeclaration(NullableStringType, Identifier("GetString")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(StringType, Identifier("name"))))) + .WithBody(Block(SingletonList( + ReturnStatement(InvocationExpression(SimpleMemberAccessExpression( + IdentifierName("ResourceManager"), + IdentifierName("GetString"))) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName("name")), + Argument(IdentifierName("Culture")), + ]))))))); + } + + private static IEnumerable GenerateEntryMemberDeclarations(SourceProductionContext production, ResxGeneratorContext context) + { + foreach (ResxEntry entry in context.Entries) + { + SyntaxTriviaList comment = GenerateCommentForEntry(entry); + + yield return PropertyDeclaration(StringType, entry.Name) + .WithModifiers(PublicStaticTokenList) + .WithLeadingTrivia(comment) + .WithExpressionBody(ArrowExpressionClause(SuppressNullableWarningExpression( + InvocationExpression(IdentifierName("GetString")) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(StringLiteralExpression(entry.Name)))))))) + .WithSemicolonToken(SemicolonToken); + + string? value = entry.Values.FirstOrDefault().Value; + if (string.IsNullOrEmpty(value)) + { + continue; + } + + if (!CompositeFormat.TryParse(entry.Values.First().Value!, out CompositeFormat? compositeFormat) || + compositeFormat.MinimumArgumentCount <= 0) + { + continue; + } + + if (entry.Name.EndsWith("Format", StringComparison.OrdinalIgnoreCase)) + { + production.ReportDiagnostic(Diagnostic.Create(NameShouldNotEndsWithFormat, Location.None, entry.Name)); + } + + int argsCount = compositeFormat.MinimumArgumentCount; + yield return MethodDeclaration(StringType, Identifier($"Format{entry.Name}")) + .WithModifiers(PublicStaticTokenList) + .WithLeadingTrivia(ParseLeadingTrivia($""" + /// + + """)) + .WithParameterList(ParameterList(SeparatedList(GenerateFormatMethodParameters(argsCount)))) + .WithExpressionBody(ArrowExpressionClause(SuppressNullableWarningExpression( + InvocationExpression(IdentifierName("GetString")) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(StringLiteralExpression(entry.Name)), + .. GenerateFormatMethodArguments(argsCount) + ])))))) + .WithSemicolonToken(SemicolonToken); + } + } + + private static SyntaxTriviaList GenerateCommentForEntry(ResxEntry entry) + { + XElement summary = new("summary", new XElement("para", $"Looks up a localized string for \"{entry.Name}\".")); + if (!string.IsNullOrWhiteSpace(entry.Comment)) + { + summary.Add(new XElement("para", entry.Comment)); + } + + foreach((string locale, string? each) in entry.Values) + { + summary.Add(new XElement("code", $"{locale,-8} Value: \"{each}\"")); + } + + StringBuilder builder = new StringBuilder().Append("/// "); + using (XmlWriter writer = XmlWriter.Create(builder, new() { OmitXmlDeclaration = true })) + { + summary.WriteTo(writer); + } + + builder.Replace("\r\n", "\r\n/// ").AppendLine(); + SyntaxTriviaList comment = ParseLeadingTrivia(builder.ToString()); + return comment; + } + + private static IEnumerable GenerateFormatMethodParameters(int argsCount) + { + for (int i = 0; i < argsCount; i++) + { + yield return Parameter(Identifier($"arg{i}")) + .WithType(NullableObjectType); + } + } + + private static IEnumerable GenerateFormatMethodArguments(int argsCount) + { + for (int i = 0; i < argsCount; i++) + { + yield return Argument(IdentifierName($"arg{i}")); + } + } + + private static CompilationUnitSyntax GenerateNameEnumCompilationUnit(ResxGeneratorContext context) + { + return CompilationUnit() + .WithMembers(SingletonList(FileScopedNamespaceDeclaration(context.Namespace!) + .WithMembers(SingletonList( + EnumDeclaration($"{context.ClassName}Name") + .WithModifiers(InternalTokenList) + .WithLeadingTrivia(NullableEnableTriviaList) + .WithMembers(SeparatedList(GenerateEnumMembers(context))))))) + .NormalizeWhitespace(); + } + + private static IEnumerable GenerateEnumMembers(ResxGeneratorContext context) + { + foreach (ResxEntry entry in context.Entries) + { + yield return EnumMemberDeclaration(entry.Name) + .WithLeadingTrivia(GenerateCommentForEntry(entry)); + } + } + + private static AttributeListSyntax ReturnNotNullIfNotNullAttributeList(string parameterName) + { + return AttributeList(SingletonSeparatedList( + Attribute(NameOfSystemDiagnosticsCodeAnalysisNotNullIfNotNull) + .WithArgumentList(AttributeArgumentList(SingletonSeparatedList( + AttributeArgument(NameOfExpression(IdentifierName(parameterName)))))))) + .WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.ReturnKeyword))); + } + + private static ParameterSyntax NullableParamsArrayOfNullableObjectTypeParameter(string name) + { + return Parameter(Identifier(name)) + .WithType(NullableType(ArrayType(NullableObjectType) + .WithRankSpecifiers(SingletonList( + ArrayRankSpecifier(SingletonSeparatedList( + OmittedArraySizeExpression())))))) + .WithModifiers(ParamsTokenList); + } + + private sealed record ResxGeneratorContext + { + public required string? Namespace { get; init; } + + public required string? ClassName { get; init; } + + public required string? ResourceName { get; init; } + + public required EquatableArray Entries { get; init; } + + public static ResxGeneratorContext Create(((string Namespace, string ClassName, string ResourceName) Key, EquatableArray Entries) source, CancellationToken token) + { + List entryBuilders = []; + foreach (ResxFile? file in source.Entries) + { + foreach (ResxData? data in file.DataArray) + { + if (entryBuilders.Find(entry => entry.Name == data.Name) is not { } existingEntry) + { + existingEntry = ResxEntry.CreateBuilder(data.Name, data.Type, data.Comment); + entryBuilders.Add(existingEntry); + } + + existingEntry.Add(file.Locale, data.Value); + } + } + + entryBuilders.Sort(static (x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); + + return new() + { + Namespace = source.Key.Namespace, + ClassName = source.Key.ClassName, + ResourceName = source.Key.ResourceName, + Entries = entryBuilders.Select(builder => builder.ToEntry()).ToImmutableArray(), + }; + } + } + + private sealed record ResxEntry + { + public required string Name { get; init; } + + public required string? Type { get; init; } + + public required string? Comment { get; init; } + + public required EquatableArray<(string Locale, string? Value)> Values { get; init; } + + public static Builder CreateBuilder(string name, string? type = null, string? comment = null) + { + return new() + { + Name = name, + Type = type, + Comment = comment, + }; + } + + public sealed class Builder + { + private readonly List<(string Locale, string? Value)> values = []; + + public required string Name { get; init; } + + public string? Type { get; init; } + + public string? Comment { get; init; } + + public void Add(string locale, string? value) + { + values.Add((locale, value)); + } + + public ResxEntry ToEntry() + { + values.Sort(static (x, y) => + { + if (string.Equals(x.Locale, y.Locale, StringComparison.Ordinal)) + { + return 0; + } + + if (x.Locale == "Neutral") + { + return -1; + } + + if (y.Locale == "Neutral") + { + return 1; + } + + return string.Compare(x.Locale, y.Locale, StringComparison.Ordinal); + }); + + return new() + { + Name = Name, + Type = Type, + Comment = Comment, + Values = values.ToImmutableArray(), + }; + } + } + } + + private sealed record ResxFile + { + public required string ResourcePath { get; init; } + + public required string Locale { get; init; } + + public required EquatableArray DataArray { get; init; } + + public required string? Namespace { get; init; } + + public required string? ClassName { get; init; } + + public required string? ResourceName { get; init; } + + public static ResxFile Create(AdditionalText text, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string? assemblyName, CancellationToken token) + { + SourceText? content = text.GetText(token); + if (content is null) + { + return default!; + } + + ImmutableArray.Builder resxDataBuilder = ImmutableArray.CreateBuilder(); + + try + { + XDocument document = XDocument.Parse(content.ToString()); + foreach (XElement? element in document.XPathSelectElements("/root/data")) + { + string? name = element.Attribute("name")?.Value; + string? type = element.Attribute("type")?.Value; + string? comment = element.Element("comment")?.Value; + string? value = element.Element("value")?.Value; + + if (name is not null) + { + ResxData resxData = new() + { + Name = name, + Type = type, + Comment = comment, + Value = value, + }; + + resxDataBuilder.Add(resxData); + } + } + } + catch + { + return default!; + } + + string resourcePath = GetResourcePath(text.Path); + + string? metadataRootNamespace = GetMetadataValue(text, analyzerConfigOptionsProvider, "RootNamespace", "RootNamespace"); + string? metadataProjectDir = GetMetadataValue(text, analyzerConfigOptionsProvider, "ProjectDir", "ProjectDir"); + string? metadataNamespace = GetMetadataValue(text, analyzerConfigOptionsProvider, "Namespace", "DefaultResourcesNamespace"); + string? metadataResourceName = GetMetadataValue(text, analyzerConfigOptionsProvider, "ResourceName", null); + string? metadataClassName = GetMetadataValue(text, analyzerConfigOptionsProvider, "ClassName", null); + + string? rootNamespace = metadataRootNamespace ?? assemblyName ?? string.Empty; + string? projectDir = metadataProjectDir ?? assemblyName ?? string.Empty; + string? defaultResourceName = ComputeResourceName(rootNamespace, projectDir, resourcePath); + string? defaultNamespace = ComputeNamespace(rootNamespace, projectDir, resourcePath); + + return new() + { + ResourcePath = resourcePath, + Locale = GetLocaleName(text.Path), + DataArray = resxDataBuilder.ToImmutable(), + Namespace = metadataNamespace ?? defaultNamespace, + ResourceName = metadataResourceName ?? defaultResourceName, + ClassName = metadataClassName ?? ToCSharpNameIdentifier(Path.GetFileName(resourcePath)) + }; + } + + private static string GetResourcePath(string path) + { + string pathWithoutExtension = Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path)); + int indexOf = pathWithoutExtension.LastIndexOf('.'); + if (indexOf < 0) + { + return pathWithoutExtension; + } + + try + { + _ = CultureInfo.GetCultureInfo(pathWithoutExtension[(indexOf + 1)..]); + return pathWithoutExtension[..indexOf]; + } + catch + { + return pathWithoutExtension; + } + } + + private static string GetLocaleName(string path) + { + string fileName = Path.GetFileNameWithoutExtension(path); + int indexOf = fileName.LastIndexOf('.'); + return indexOf < 0 ? "Neutral" : fileName[(indexOf + 1)..]; + } + + private static string? GetMetadataValue(AdditionalText file, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, string? globalName) + { + string? result = null; + if (analyzerConfigOptionsProvider.GetOptions(file).TryGetValue($"build_metadata.AdditionalFiles.{name}", out string? value)) + { + if (value != result) + { + return default; + } + + result = value; + } + + if (!string.IsNullOrEmpty(result)) + { + return result; + } + + if (globalName is not null && analyzerConfigOptionsProvider.GlobalOptions.TryGetValue($"build_property.{globalName}", out string? globalValue) && !string.IsNullOrEmpty(globalValue)) + { + return globalValue; + } + + return default; + } + + private static string? ComputeResourceName(string rootNamespace, string projectDir, string resourcePath) + { + string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir)); + string fullResourcePath = Path.GetFullPath(resourcePath); + + if (fullProjectDir == fullResourcePath) + { + return rootNamespace; + } + + if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal)) + { + string relativePath = fullResourcePath[fullProjectDir.Length..]; + return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.'); + } + + return null; + } + + private static string? ComputeNamespace(string rootNamespace, string projectDir, string resourcePath) + { + string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir)); + string fullResourcePath = EnsureEndSeparator(Path.GetDirectoryName(Path.GetFullPath(resourcePath))!); + + if (fullProjectDir == fullResourcePath) + { + return rootNamespace; + } + + if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal)) + { + string relativePath = fullResourcePath.Substring(fullProjectDir.Length); + return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.').TrimEnd('.'); + } + + return null; + } + + private static string ToCSharpNameIdentifier(string name) + { + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#identifiers + // https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory?view=net-5.0 + StringBuilder sb = new(); + foreach (char c in name) + { + UnicodeCategory category = char.GetUnicodeCategory(c); + switch (category) + { + case UnicodeCategory.UppercaseLetter: + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.TitlecaseLetter: + case UnicodeCategory.ModifierLetter: + case UnicodeCategory.OtherLetter: + case UnicodeCategory.LetterNumber: + sb.Append(c); + break; + + case UnicodeCategory.DecimalDigitNumber: + case UnicodeCategory.ConnectorPunctuation: + case UnicodeCategory.Format: + if (sb.Length == 0) + { + sb.Append('_'); + } + sb.Append(c); + break; + + default: + sb.Append('_'); + break; + } + } + + return sb.ToString(); + } + + private static string EnsureEndSeparator(string path) + { + if (path[^1] == Path.DirectorySeparatorChar) + { + return path; + } + + return path + Path.DirectorySeparatorChar; + } + } + + private sealed record ResxData + { + public required string Name { get; init; } + + public string? Type { get; init; } + + public string? Value { get; init; } + + public string? Comment { get; init; } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/ValueStringBuilder.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/ValueStringBuilder.cs new file mode 100644 index 0000000..60830f9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Resx/ValueStringBuilder.cs @@ -0,0 +1,318 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.SourceGeneration.Resx; + +// Ported System.Text.ValueStringBuilder from dotnet/runtime +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +internal ref struct ValueStringBuilder +{ + private char[]? arrayToReturnToPool; + private Span chars; + private int pos; + + public ValueStringBuilder(Span initialBuffer) + { + arrayToReturnToPool = null; + chars = initialBuffer; + pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + chars = arrayToReturnToPool; + pos = 0; + } + + public int Length + { + get => pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= chars.Length); + pos = value; + } + } + + public int Capacity { get => chars.Length; } + + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)chars.Length) + Grow(capacity - pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < pos); + return ref chars[index]; + } + } + + public override string ToString() + { + string s = chars[..pos].ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars { get => chars; } + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + chars[Length] = '\0'; + } + return chars[..pos]; + } + + public ReadOnlySpan AsSpan() => chars.Slice(0, pos); + public ReadOnlySpan AsSpan(int start) => chars.Slice(start, pos - start); + public ReadOnlySpan AsSpan(int start, int length) => chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (chars[..pos].TryCopyTo(destination)) + { + charsWritten = pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (pos > chars.Length - count) + { + Grow(count); + } + + int remaining = pos - index; + chars.Slice(index, remaining).CopyTo(chars[(index + count)..]); + chars.Slice(index, count).Fill(value); + pos += count; + } + + public void Insert(int index, string? s) + { + if (s == null) + { + return; + } + + int count = s.Length; + + if (pos > (chars.Length - count)) + { + Grow(count); + } + + int remaining = pos - index; + chars.Slice(index, remaining).CopyTo(chars[(index + count)..]); + s +#if !NET + .AsSpan() +#endif + .CopyTo(chars[index..]); + pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = this.pos; + Span chars = this.chars; + if ((uint)pos < (uint)chars.Length) + { + chars[pos] = c; + this.pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) + { + if (s == null) + { + return; + } + + int pos = this.pos; + if (s.Length == 1 && (uint)pos < (uint)chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + chars[pos] = s[0]; + this.pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + private void AppendSlow(string s) + { + int pos = this.pos; + if (pos > chars.Length - s.Length) + { + Grow(s.Length); + } + + s +#if !NET + .AsSpan() +#endif + .CopyTo(chars[pos..]); + this.pos += s.Length; + } + + public void Append(char c, int count) + { + if (pos > chars.Length - count) + { + Grow(count); + } + + Span dst = chars.Slice(pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + pos += count; + } + + public void Append(scoped ReadOnlySpan value) + { + int pos = this.pos; + if (pos > chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(chars[this.pos..]); + this.pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = pos; + if (origPos > chars.Length - length) + { + Grow(length); + } + + pos = origPos + length; + return chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(pos > chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + int newCapacity = (int)Math.Max( + (uint)(pos + additionalCapacityBeyondPos), + Math.Min((uint)chars.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + char[] poolArray = ArrayPool.Shared.Rent(newCapacity); + + chars[..pos].CopyTo(poolArray); + + char[]? toReturn = arrayToReturnToPool; + chars = arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + char[]? toReturn = arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj new file mode 100644 index 0000000..6463874 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj @@ -0,0 +1,57 @@ + + + + true + netstandard2.0 + preview + enable + AnyCPU + true + Debug;Release + true + + + + + all + + + + + + + + + + + + + + + + + + + + + + + + + + + Snap.Hutao.SourceGeneration + 1.3.14 + DGP Studio + false + true + MIT + Source Code generator for Snap.Hutao + https://github.com/DGP-Studio/Snap.Hutao.SourceGeneration + https://github.com/DGP-Studio/Snap.Hutao.SourceGeneration + + + true + + + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs new file mode 100644 index 0000000..0a8efd7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs @@ -0,0 +1,164 @@ +// 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 Microsoft.CodeAnalysis.Diagnostics; +using Snap.Hutao.SourceGeneration.Extension; +using System.Collections.Immutable; + +namespace Snap.Hutao.SourceGeneration; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class UniversalAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor TypeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal or private", "Quality", DiagnosticSeverity.Info, true); + // SH002: ReadOnly struct should be passed with ref-like key word + private static readonly DiagnosticDescriptor UseValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true); + // SH004: Use "is not null" instead of "!= null" whenever possible + // SH005: Use "is null" instead of "== null" whenever possible + // SH006: Use "is { } obj" whenever possible + private static readonly DiagnosticDescriptor UseArgumentNullExceptionThrowIfNullDescriptor = new("SH007", "Use \"ArgumentNullException.ThrowIfNull()\" instead of \"!\"", "Use \"ArgumentNullException.ThrowIfNull()\"", "Quality", DiagnosticSeverity.Info, true); + private static readonly DiagnosticDescriptor CastCanBeSlowDescriptor = new("SH008", "Cast can be slow", "Cast can be slow, consider use Unsafe.As or Unsafe.Unbox", "Quality", DiagnosticSeverity.Info, true); + // SH020: File header mismatch + // SH100: Use "IContentDialogFactory.EnqueueAndShowAsync" instead + + public override ImmutableArray SupportedDiagnostics + { + get => + [ + TypeInternalDescriptor, + UseValueTaskIfPossibleDescriptor, + UseArgumentNullExceptionThrowIfNullDescriptor, + CastCanBeSlowDescriptor + ]; + } + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(CompilationStart); + } + + private static void CompilationStart(CompilationStartAnalysisContext context) + { + context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, SyntaxKind.ClassDeclaration, SyntaxKind.InterfaceDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.EnumDeclaration); + context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration); + context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression); + context.RegisterSyntaxNodeAction(HandleCastCanBeSlow, SyntaxKind.CastExpression); + } + + // SH001 + private static void HandleTypeShouldBeInternal(SyntaxNodeAnalysisContext context) + { + BaseTypeDeclarationSyntax syntax = (BaseTypeDeclarationSyntax)context.Node; + + bool privateExists = false; + bool internalExists = false; + bool fileExists = false; + + foreach (SyntaxToken token in syntax.Modifiers) + { + if (token.IsKind(SyntaxKind.PrivateKeyword)) + { + privateExists = true; + break; + } + + if (token.IsKind(SyntaxKind.InternalKeyword)) + { + internalExists = true; + break; + } + + if (token.IsKind(SyntaxKind.FileKeyword)) + { + fileExists = true; + break; + } + } + + if (!privateExists && !internalExists && !fileExists) + { + Location location = syntax.Identifier.GetLocation(); + Diagnostic diagnostic = Diagnostic.Create(TypeInternalDescriptor, location, syntax.Identifier); + context.ReportDiagnostic(diagnostic); + } + } + + // SH003 + private static void HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask(SyntaxNodeAnalysisContext context) + { + MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node; + IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!; + + // 跳过重载方法 + if (methodSymbol.IsOverride) + { + return; + } + + // AsyncRelayCommand backing method can only use Task or Task + if (methodSymbol.HasAttributeWithFullyQualifiedMetadataName(WellKnownMetadataNames.CommandAttribute)) + { + return; + } + + if (methodSymbol.ReturnType.HasOrInheritsFromFullyQualifiedMetadataName("System.Threading.Tasks.Task")) + { + Location location = methodSyntax.ReturnType.GetLocation(); + Diagnostic diagnostic = Diagnostic.Create(UseValueTaskIfPossibleDescriptor, location); + context.ReportDiagnostic(diagnostic); + } + } + + // SH007 + private static void HandleArgumentNullExceptionThrowIfNull(SyntaxNodeAnalysisContext context) + { + if (context.Node is not PostfixUnaryExpressionSyntax syntax || syntax.Kind() is not SyntaxKind.SuppressNullableWarningExpression) + { + return; + } + + // default! + if (syntax.Operand is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.DefaultLiteralExpression)) + { + return; + } + + // default(?)! + if (syntax.Operand is DefaultExpressionSyntax) + { + return; + } + + Location location = syntax.GetLocation(); + Diagnostic diagnostic = Diagnostic.Create(UseArgumentNullExceptionThrowIfNullDescriptor, location); + context.ReportDiagnostic(diagnostic); + } + + // SH008 + private static void HandleCastCanBeSlow(SyntaxNodeAnalysisContext context) + { + CastExpressionSyntax syntax = (CastExpressionSyntax)context.Node; + + if (syntax.Expression.Kind() is SyntaxKind.CollectionExpression) + { + return; + } + + TypeInfo targetType = context.SemanticModel.GetTypeInfo(syntax.Type); + TypeInfo expressionType = context.SemanticModel.GetTypeInfo(syntax.Expression); + if (targetType.Type?.IsValueType is not false && expressionType.Type?.IsValueType is not false) // null or true + { + return; + } + + Location location = syntax.GetLocation(); + Diagnostic diagnostic = Diagnostic.Create(CastCanBeSlowDescriptor, location); + context.ReportDiagnostic(diagnostic); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/WellKnownMetadataNames.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/WellKnownMetadataNames.cs new file mode 100644 index 0000000..88778af --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/WellKnownMetadataNames.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.SourceGeneration; + +internal static class WellKnownMetadataNames +{ + public const string CommandAttribute = "Snap.Hutao.Core.Annotation.CommandAttribute"; + public const string GeneratedConstructorAttribute = "Snap.Hutao.Core.Annotation.GeneratedConstructorAttribute"; + public const string BindableCustomPropertyProviderAttribute = "Snap.Hutao.Core.Annotation.BindableCustomPropertyProviderAttribute"; + public const string DependencyPropertyAttributeT = "Snap.Hutao.Core.Annotation.DependencyPropertyAttribute`1"; + public const string FieldAccessAttribute = "Snap.Hutao.Core.Annotation.FieldAccessorAttribute"; + + public const string HttpClient = "System.Net.Http.HttpClient"; + public const string HttpClientAttribute = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute"; + public const string PrimaryHttpMessageHandlerAttribute = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute"; + public const string HttpClientConfiguration = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration."; + + public const string IServiceProvider = "System.IServiceProvider"; + public const string ServiceLifetimeSingleton = "Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton"; + public const string ServiceLifetimeScoped = "Microsoft.Extensions.DependencyInjection.ServiceLifetime.Scoped"; + public const string ServiceLifetimeTransient = "Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient"; + + public const string ServiceAttribute = "Snap.Hutao.Core.DependencyInjection.Annotation.ServiceAttribute"; + public const string FromKeyedServicesAttribute = "Snap.Hutao.Core.DependencyInjection.Annotation.FromKeyedServicesAttribute"; + + public const string ExtendedEnumAttribute = "Snap.Hutao.Resource.Localization.ExtendedEnumAttribute"; + public const string LocalizationKeyAttribute = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute"; + public const string InterceptsLocationAttribute = "System.Runtime.CompilerServices.InterceptsLocationAttribute"; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/WellKnownSyntax.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/WellKnownSyntax.cs new file mode 100644 index 0000000..92be81d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/WellKnownSyntax.cs @@ -0,0 +1,95 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory; + +namespace Snap.Hutao.SourceGeneration; + +internal static class WellKnownSyntax +{ + // [global::JetBrains.Annotations.MeansImplicitUse] + public static readonly AttributeListSyntax JetBrainsAnnotationsMeansImplicitUseAttributeList = AttributeList(SingletonSeparatedList(Attribute(ParseName("global::JetBrains.Annotations.MeansImplicitUse")))); + + // : global::System.Attribute + public static readonly BaseListSyntax SystemAttributeBaseList = BaseList(SingletonSeparatedList(SimpleBaseType(ParseTypeName("global::System.Attribute")))); + + // throw new NotSupportedException() + public static readonly ExpressionSyntax ThrowNotSupportedException = ThrowExpression(ObjectCreationExpression(IdentifierName("NotSupportedException")).WithEmptyArgumentList()); + + public static readonly NameSyntax NameOfCommunityToolkitMvvmInput = ParseName("global::CommunityToolkit.Mvvm.Input"); + public static readonly NameSyntax NameOfCommunityToolkitMvvmInputAsyncRelayCommandOptions = ParseName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"); + public static readonly NameSyntax NameOfMicrosoftUIXaml = ParseName("global::Microsoft.UI.Xaml"); + public static readonly NameSyntax NameOfSnapHutaoModelPrimitiveConverter = ParseName("global::Snap.Hutao.Model.Primitive.Converter"); + public static readonly NameSyntax NameOfSystem = ParseName("global::System"); + public static readonly NameSyntax NameOfSystemComponentModelEditorBrowsable = ParseName("global::System.ComponentModel.EditorBrowsable"); + public static readonly NameSyntax NameOfSystemComponentModelEditorBrowsableState = ParseName("global::System.ComponentModel.EditorBrowsableState"); + public static readonly NameSyntax NameOfSystemDiagnosticsCodeAnalysisMaybeNull = ParseName("global::System.Diagnostics.CodeAnalysis.MaybeNull"); + public static readonly NameSyntax NameOfSystemDiagnosticsCodeAnalysisNotNullIfNotNull = ParseName("global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull"); + public static readonly NameSyntax NameOfSystemNumerics = ParseName("global::System.Numerics"); + public static readonly NameSyntax NameOfSystemRuntimeCompilerServicesUnsafeAccessor = ParseName("global::System.Runtime.CompilerServices.UnsafeAccessor"); + public static readonly NameSyntax NameOfSystemRuntimeCompilerServicesUnsafeAccessorKind = ParseName("global::System.Runtime.CompilerServices.UnsafeAccessorKind"); + public static readonly NameSyntax NameOfSystemTextJsonSerializationJsonConverter = ParseName("global::System.Text.Json.Serialization.JsonConverter"); + + public static readonly TypeSyntax TypeOfCommunityToolkitMvvmMessagingIMessenger = ParseTypeName("global::CommunityToolkit.Mvvm.Messaging.IMessenger"); + public static readonly TypeSyntax TypeOfCommunityToolkitMvvmMessagingIMessengerExtensions = ParseTypeName("global::CommunityToolkit.Mvvm.Messaging.IMessengerExtensions"); + public static readonly TypeSyntax TypeOfMicrosoftExtensionsDependencyInjectionIServiceCollection = ParseTypeName("global::Microsoft.Extensions.DependencyInjection.IServiceCollection"); + public static readonly TypeSyntax TypeOfMicrosoftExtensionsDependencyInjectionServiceLifetime = ParseTypeName("global::Microsoft.Extensions.DependencyInjection.ServiceLifetime"); + public static readonly TypeSyntax TypeOfSystemArgumentException = ParseTypeName("global::System.ArgumentException"); + public static readonly TypeSyntax TypeOfSystemArgumentNullException = ParseTypeName("global::System.ArgumentNullException"); + public static readonly TypeSyntax TypeOfSystemAttributeTargets = ParseTypeName("global::System.AttributeTargets"); + public static readonly TypeSyntax TypeOfSystemRuntimeCompilerServicesUnsafe = ParseTypeName("global::System.Runtime.CompilerServices.Unsafe"); + public static readonly TypeSyntax TypeOfSystemEnum = ParseTypeName("global::System.Enum"); + public static readonly TypeSyntax TypeOfSystemGlobalizationCultureInfo = ParseTypeName("global::System.Globalization.CultureInfo"); + public static readonly TypeSyntax TypeOfSystemIComparable = ParseTypeName("global::System.IComparable"); + public static readonly TypeSyntax TypeOfSystemIOStream = ParseTypeName("global::System.IO.Stream"); + public static readonly TypeSyntax TypeOfSystemIServiceProvider = ParseTypeName("global::System.IServiceProvider"); + public static readonly TypeSyntax TypeOfSystemNetHttpHttpClient = ParseTypeName("global::System.Net.Http.HttpClient"); + public static readonly TypeSyntax TypeOfSystemNetHttpIHttpClientFactory = ParseTypeName("global::System.Net.Http.IHttpClientFactory"); + public static readonly TypeSyntax TypeOfSystemNetHttpSocketsHttpHandler = ParseTypeName("global::System.Net.Http.SocketsHttpHandler"); + public static readonly TypeSyntax TypeOfSystemResourcesResourceManager = ParseTypeName("global::System.Resources.ResourceManager"); + public static readonly TypeSyntax TypeOfSystemType = ParseTypeName("global::System.Type"); + + public static readonly AttributeArgumentSyntax AttributeTargetsClass = AttributeArgument(SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Class"))); + public static readonly AttributeArgumentSyntax AttributeTargetsConstructor = AttributeArgument(SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Constructor"))); + public static readonly AttributeArgumentSyntax AttributeTargetsEnum = AttributeArgument(SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Enum"))); + public static readonly AttributeArgumentSyntax AttributeTargetsField = AttributeArgument(SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Field"))); + public static readonly AttributeArgumentSyntax AttributeTargetsFieldAndProperty = AttributeArgument(BitwiseOrExpression( + SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Field")), + SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Property")))); + public static readonly AttributeArgumentSyntax AttributeTargetsMethod = AttributeArgument(SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Method"))); + public static readonly AttributeArgumentSyntax AttributeTargetsProperty = AttributeArgument(SimpleMemberAccessExpression(TypeOfSystemAttributeTargets, IdentifierName("Property"))); + + public static readonly AttributeArgumentSyntax AllowMultipleTrue = AttributeArgument(TrueLiteralExpression).WithNameEquals(NameEquals(IdentifierName("AllowMultiple"))); + public static readonly AttributeArgumentSyntax InheritedFalse = AttributeArgument(FalseLiteralExpression).WithNameEquals(NameEquals(IdentifierName("Inherited"))); + + // ArgumentNullException.ThrowIfNull(%argumentExpression%) + public static InvocationExpressionSyntax ArgumentNullExceptionThrowIfNull(ExpressionSyntax argumentExpression) + { + return InvocationExpression( + SimpleMemberAccessExpression( + TypeOfSystemArgumentNullException, + IdentifierName("ThrowIfNull"))) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(argumentExpression)))); + } + + // %serviceProvider%.GetRequiredService<%type%>() + public static InvocationExpressionSyntax ServiceProviderGetRequiredService(ExpressionSyntax serviceProvider, TypeSyntax type) + { + return InvocationExpression(SimpleMemberAccessExpression( + serviceProvider, + GenericName("GetRequiredService").WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(type))))) + .WithEmptyArgumentList(); + } + + // %serviceProvider%.GetRequiredKeyedService<%type%>(%argument%) + public static InvocationExpressionSyntax ServiceProviderGetRequiredKeyedService(ExpressionSyntax serviceProvider, TypeSyntax type, ExpressionSyntax argument) + { + return InvocationExpression(SimpleMemberAccessExpression( + serviceProvider, + GenericName("GetRequiredKeyedService").WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(type))))) + .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument(argument)))); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/BindableCustomPropertyGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/BindableCustomPropertyGenerator.cs new file mode 100644 index 0000000..8d0ecf5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/BindableCustomPropertyGenerator.cs @@ -0,0 +1,331 @@ +// 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.Threading; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory; +using static Snap.Hutao.SourceGeneration.WellKnownSyntax; + +namespace Snap.Hutao.SourceGeneration.Xaml; + +[Generator(LanguageNames.CSharp)] +internal sealed class BindableCustomPropertyGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.BindableCustomPropertyProviderAttribute, + SyntaxNodeHelper.Is, + BindableCustomPropertyGeneratorContext.Create) + .Where(context => context is not null); + + context.RegisterImplementationSourceOutput(provider, GenerateWrapper); + } + + private static void GenerateWrapper(SourceProductionContext production, BindableCustomPropertyGeneratorContext context) + { + try + { + Generate(production, context); + } + catch (Exception ex) + { + production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", ex.ToString()); + } + } + + private static void Generate(SourceProductionContext production, BindableCustomPropertyGeneratorContext context) + { + TypeSyntax baseType = ParseTypeName("global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation"); + TypeSyntax bindableCustomPropertyType = ParseTypeName("global::Microsoft.UI.Xaml.Data.BindableCustomProperty"); + + TypeSyntax type = ParseTypeName(context.Hierarchy.Hierarchy[0].FullyQualifiedName); + + CompilationUnitSyntax syntax = context.Hierarchy.GetCompilationUnit( + [ + // GetProperty(string) + MethodDeclaration(NullableType(bindableCustomPropertyType), Identifier("GetProperty")) + .WithModifiers(PublicTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(StringType, Identifier("name"))))) + .WithBody(Block(SingletonList( + ReturnStatement(SwitchExpression(IdentifierName("name")) + .WithArms(SeparatedList(GenerateGetPropertySwitchExpressionArms(type, context.Properties, context.Methods))))))), + + // GetProperty(Type) + MethodDeclaration(NullableType(bindableCustomPropertyType), Identifier("GetProperty")) + .WithModifiers(PublicTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(TypeOfSystemType, Identifier("indexParameterType"))))) + .WithBody(Block(List(GenerateGetIndexerStatements(type, context.Properties)))) + ], + BaseList(SingletonSeparatedList(SimpleBaseType(baseType)))) + .NormalizeWhitespace(); + + production.AddSource(context.Hierarchy.FileNameHint, syntax.ToFullStringWithHeader()); + } + + private static IEnumerable GenerateGetPropertySwitchExpressionArms(TypeSyntax ownerType, EquatableArray properties, EquatableArray methods) + { + foreach (PropertyInfo property in properties) + { + if (property.IsIndexer) + { + continue; + } + + bool canRead = property.GetMethodAccessibility is Accessibility.Public; + bool canWrite = property.SetMethodAccessibility is Accessibility.Public; + + TypeSyntax propertyType = ParseTypeName(property.FullyQualifiedTypeName); + + ExpressionSyntax getValue = canRead + ? SimpleLambdaExpression(Parameter(Identifier("instance"))) + .WithModifiers(StaticTokenList) + .WithExpressionBody(SimpleMemberAccessExpression(property.IsStatic + ? ownerType + : UnsafeAsExpression(ownerType, IdentifierName("instance")), + IdentifierName(property.Name))) + : DefaultLiteralExpression; + + ExpressionSyntax setValue = canWrite + ? ParenthesizedLambdaExpression() + .WithModifiers(StaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(Identifier("instance")), + Parameter(Identifier("value")) + ]))) + .WithBody(Block(List( + [ + LocalDeclarationStatement(VariableDeclaration(ownerType, SingletonSeparatedList(VariableDeclarator("typedInstance") + .WithInitializer(EqualsValueClause(UnsafeAsExpression(ownerType, IdentifierName("instance"))))))), + IfStatement( + SimpleMemberAccessExpression(SimpleMemberAccessExpression(IdentifierName("typedInstance"), IdentifierName("IsViewUnloaded")), IdentifierName("Value")), + Block(ReturnStatement())), + ExpressionStatement(SimpleAssignmentExpression( + SimpleMemberAccessExpression( + property.IsStatic ? ownerType : IdentifierName("typedInstance"), + IdentifierName(property.Name)), + UnsafeUnboxOrAsExpression(propertyType, IdentifierName("value"), property.TypeIsValueType))) + ]))) + : DefaultLiteralExpression; + + yield return SwitchExpressionArm( + ConstantPattern(NameOfExpression(IdentifierName(property.Name))), + ImplicitObjectCreationExpression() + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(LiteralExpression(canRead)), // canRead + Argument(LiteralExpression(canWrite)), // canWrite + Argument(NameOfExpression(IdentifierName(property.Name))), // name + Argument(TypeOfExpression(propertyType)), // type + Argument(getValue), // getValue + Argument(setValue), // setValue + Argument(DefaultLiteralExpression), // getIndexedValue + Argument(DefaultLiteralExpression) // setIndexedValue + ])))); + } + + foreach (AttributedMethodInfo method in methods) + { + foreach (AttributeInfo attribute in method.Attributes) + { + if (attribute.FullyQualifiedMetadataName is not WellKnownMetadataNames.CommandAttribute) + { + continue; + } + + if (!attribute.TryGetConstructorArgument(0, out string? commandName)) + { + continue; + } + + ExpressionSyntax getValue = SimpleLambdaExpression(Parameter(Identifier("instance"))) + .WithModifiers(StaticTokenList) + .WithExpressionBody(SimpleMemberAccessExpression(method.Method.IsStatic + ? ownerType + : UnsafeAsExpression(ownerType, IdentifierName("instance")), + IdentifierName(commandName))); + + yield return SwitchExpressionArm( + ConstantPattern(NameOfExpression(IdentifierName(commandName))), + ImplicitObjectCreationExpression() + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(LiteralExpression(true)), // canRead + Argument(LiteralExpression(false)), // canWrite + Argument(NameOfExpression(IdentifierName(commandName))), // name + Argument(TypeOfExpression(CommandHelper.GetCommandType(method))), // type + Argument(getValue), // getValue + Argument(DefaultLiteralExpression), // setValue + Argument(DefaultLiteralExpression), // getIndexedValue + Argument(DefaultLiteralExpression) // setIndexedValue + ])))); + } + } + + yield return SwitchExpressionArm( + DiscardPattern(), + DefaultLiteralExpression); + } + + private static IEnumerable GenerateGetIndexerStatements(TypeSyntax ownerType, EquatableArray properties) + { + foreach (PropertyInfo property in properties) + { + if (!property.IsIndexer) + { + continue; + } + + bool canRead = property.GetMethodAccessibility is Accessibility.Public; + bool canWrite = property.SetMethodAccessibility is Accessibility.Public; + + TypeSyntax propertyType = ParseTypeName(property.FullyQualifiedTypeName); + TypeSyntax indexerType = ParseTypeName(property.FullyQualifiedIndexerParameterTypeName!); + + ExpressionSyntax getIndexedValue = canRead + ? ParenthesizedLambdaExpression() + .WithModifiers(StaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(Identifier("instance")), + Parameter(Identifier("index")) + ]))) + .WithExpressionBody(ElementAccessExpression( + UnsafeAsExpression(ownerType, IdentifierName("instance")), + BracketedArgumentList(SingletonSeparatedList( + Argument(UnsafeUnboxOrAsExpression(indexerType, IdentifierName("index"), property.IndexerParameterTypeIsValueType.Value)))))) + : NullLiteralExpression; + + ExpressionSyntax setIndexedValue = canWrite + ? ParenthesizedLambdaExpression() + .WithModifiers(StaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(Identifier("instance")), + Parameter(Identifier("value")), + Parameter(Identifier("index")) + ]))) + .WithBody(Block(List( + [ + LocalDeclarationStatement(VariableDeclaration(ownerType, SingletonSeparatedList(VariableDeclarator("typedInstance") + .WithInitializer(EqualsValueClause(UnsafeAsExpression(ownerType, IdentifierName("instance"))))))), + IfStatement( + SimpleMemberAccessExpression(SimpleMemberAccessExpression(IdentifierName("typedInstance"), IdentifierName("IsViewUnloaded")), IdentifierName("Value")), + Block(ReturnStatement())), + ExpressionStatement(SimpleAssignmentExpression(ElementAccessExpression( + IdentifierName("typedInstance"), + BracketedArgumentList(SingletonSeparatedList( + Argument(UnsafeUnboxOrAsExpression(indexerType, IdentifierName("index"), property.IndexerParameterTypeIsValueType.Value))))), + UnsafeUnboxOrAsExpression(propertyType, IdentifierName("value"), property.TypeIsValueType))) + ]))) + : NullLiteralExpression; + + yield return IfStatement( + EqualsExpression( + IdentifierName("indexParameterType"), + TypeOfExpression(indexerType)), + Block(SingletonList( + ReturnStatement(ImplicitObjectCreationExpression() + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(LiteralExpression(canRead)), // canRead + Argument(LiteralExpression(canWrite)), // canWrite + Argument(StringLiteralExpression("Item")), // name + Argument(TypeOfExpression(propertyType)), // type + Argument(DefaultLiteralExpression), // getValue + Argument(DefaultLiteralExpression), // setValue + Argument(getIndexedValue), // getIndexedValue + Argument(setIndexedValue) // setIndexedValue + ]))))))); + } + + yield return ReturnStatement(DefaultLiteralExpression); + } + + private static ExpressionSyntax UnsafeUnboxOrAsExpression(TypeSyntax type, ExpressionSyntax expression, bool isValueType) + { + return isValueType + ? UnsafeUnboxExpression(type, expression) + : UnsafeAsExpression(type, expression); + } + + private static ExpressionSyntax UnsafeAsExpression(TypeSyntax type, ExpressionSyntax expression) + { + return InvocationExpression(SimpleMemberAccessExpression( + TypeOfSystemRuntimeCompilerServicesUnsafe, + GenericName("As").WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(type))))) + .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument(expression)))); + } + + private static ExpressionSyntax UnsafeUnboxExpression(TypeSyntax type, ExpressionSyntax expression) + { + return InvocationExpression(SimpleMemberAccessExpression( + TypeOfSystemRuntimeCompilerServicesUnsafe, + GenericName("Unbox").WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(type))))) + .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument(expression)))); + } + + private sealed record BindableCustomPropertyGeneratorContext + { + public required HierarchyInfo Hierarchy { get; init; } + + public required EquatableArray Properties { get; init; } + + public required EquatableArray Methods { get; init; } + + public static BindableCustomPropertyGeneratorContext Create(GeneratorAttributeSyntaxContext context, CancellationToken token) + { + if (context.TargetSymbol is not INamedTypeSymbol typeSymbol) + { + return default!; + } + + ImmutableArray.Builder propertiesBuilder = ImmutableArray.CreateBuilder(); + ImmutableArray.Builder methodsBuilder = ImmutableArray.CreateBuilder(); + + for (INamedTypeSymbol? currentSymbol = typeSymbol; currentSymbol is not null; currentSymbol = currentSymbol.BaseType) + { + foreach (ISymbol member in currentSymbol.GetMembers()) + { + switch (member.Kind) + { + case SymbolKind.Property: + if (member.DeclaredAccessibility is Accessibility.Public) + { + propertiesBuilder.Add(PropertyInfo.Create((IPropertySymbol)member)); + } + break; + case SymbolKind.Method: + IMethodSymbol methodSymbol = (IMethodSymbol)member; + if (methodSymbol.HasAttributeWithFullyQualifiedMetadataName(WellKnownMetadataNames.CommandAttribute)) + { + ImmutableArray attributes = ImmutableArray.CreateRange(methodSymbol.GetAttributes(), AttributeInfo.Create); + methodsBuilder.Add(AttributedMethodInfo.Create(attributes, MethodInfo.Create(methodSymbol))); + } + break; + } + } + } + + + return new() + { + Hierarchy = HierarchyInfo.Create(typeSymbol), + Properties = propertiesBuilder.ToImmutable(), + Methods = methodsBuilder.ToImmutable(), + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/CommandGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/CommandGenerator.cs new file mode 100644 index 0000000..1983ffe --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/CommandGenerator.cs @@ -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 Snap.Hutao.SourceGeneration.Model; +using Snap.Hutao.SourceGeneration.Primitive; +using System; +using System.Collections.Generic; +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.Xaml; + +[Generator(LanguageNames.CSharp)] +internal sealed class CommandGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.CommandAttribute, + SyntaxNodeHelper.Is, + Transform) + .GroupBy(t => t.Left, t => AttributedMethodInfo.Create(t.Right)) + .Select(CommandGeneratorContext.Create); + + context.RegisterSourceOutput(provider, GenerateWrapper); + } + + private static (HierarchyInfo Hierarchy, (EquatableArray Attribute, MethodInfo Method)) Transform(GeneratorAttributeSyntaxContext context, CancellationToken token) + { + if (context.TargetSymbol is IMethodSymbol { ContainingType: { } typeSymbol } methodSymbol) + { + return (HierarchyInfo.Create(typeSymbol), (ImmutableArray.CreateRange(context.Attributes, AttributeInfo.Create), MethodInfo.Create(methodSymbol))); + } + + return default; + } + + private static void GenerateWrapper(SourceProductionContext production, CommandGeneratorContext context) + { + try + { + Generate(production, context); + } + catch (Exception e) + { + production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString()); + } + } + + private static void Generate(SourceProductionContext production, CommandGeneratorContext context) + { + CompilationUnitSyntax syntax = context.Hierarchy.GetCompilationUnit([.. GenerateCommandProperties(context.Methods)]) + .NormalizeWhitespace(); + + production.AddSource(context.Hierarchy.FileNameHint, syntax.ToFullStringWithHeader()); + } + + private static IEnumerable GenerateCommandProperties(EquatableArray methods) + { + foreach (AttributedMethodInfo attributedMethod in methods) + { + // bool isAsync = attributedMethod.Method.FullyQualifiedReturnTypeMetadataName.StartsWith("System.Threading.Tasks.Task"); + // SyntaxToken identifier = Identifier(isAsync ? "AsyncRelayCommand" : "RelayCommand"); + // + // TypeSyntax propertyType; + // ImmutableArray parameters = attributedMethod.Method.Parameters; + // if (parameters.Length >= 1) + // { + // TypeSyntax type = ParseTypeName(parameters[0].FullyQualifiedTypeName); + // propertyType = QualifiedName(NameOfCommunityToolkitMvvmInput, GenericName(identifier).WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(type)))); + // } + // else + // { + // propertyType = QualifiedName(NameOfCommunityToolkitMvvmInput, IdentifierName(identifier)); + // } + + foreach (AttributeInfo attribute in attributedMethod.Attributes) + { + if (attribute.FullyQualifiedMetadataName is not WellKnownMetadataNames.CommandAttribute) + { + continue; + } + + if (!attribute.TryGetConstructorArgument(0, out string? commandName)) + { + continue; + } + + SeparatedSyntaxList arguments = SingletonSeparatedList( + Argument(IdentifierName(attributedMethod.Method.Name))); + + if (attribute.HasNamedArgument("AllowConcurrentExecutions", true)) + { + arguments = arguments.Add(Argument(SimpleMemberAccessExpression( + NameOfCommunityToolkitMvvmInputAsyncRelayCommandOptions, + IdentifierName("AllowConcurrentExecutions")))); + } + + yield return PropertyDeclaration(CommandHelper.GetCommandType(attributedMethod), commandName) + .WithAttributeLists(SingletonList( + AttributeList(SingletonSeparatedList( + Attribute(NameOfSystemDiagnosticsCodeAnalysisMaybeNull))) + .WithTarget(AttributeTargetSpecifier(FieldKeyword)))) + .WithModifiers(attributedMethod.Method.IsStatic ? PublicStaticTokenList: PublicTokenList) + .WithAccessorList(AccessorList(SingletonList( + GetAccessorDeclaration() + .WithExpressionBody(ArrowExpressionClause(CoalesceAssignmentExpression( + FieldExpression(), + ImplicitObjectCreationExpression() + .WithArgumentList(ArgumentList(arguments)))))))); + } + } + } + + private sealed record CommandGeneratorContext + { + public required HierarchyInfo Hierarchy { get; init; } + + public required EquatableArray Methods { get; init; } + + public static CommandGeneratorContext Create((HierarchyInfo Hierarchy, EquatableArray Methods) tuple, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + return new() + { + Hierarchy = tuple.Hierarchy, + Methods = tuple.Methods, + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/CommandHelper.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/CommandHelper.cs new file mode 100644 index 0000000..bae44e3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/CommandHelper.cs @@ -0,0 +1,33 @@ +// 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.Model; +using System.Collections.Immutable; + +namespace Snap.Hutao.SourceGeneration.Xaml; + +internal static class CommandHelper +{ + public static TypeSyntax GetCommandType(AttributedMethodInfo attributedMethod) + { + bool isAsync = attributedMethod.Method.FullyQualifiedReturnTypeMetadataName.StartsWith("System.Threading.Tasks.Task"); + SyntaxToken identifier = SyntaxFactory.Identifier(isAsync ? "AsyncRelayCommand" : "RelayCommand"); + + TypeSyntax propertyType; + ImmutableArray parameters = attributedMethod.Method.Parameters; + if (parameters.Length >= 1) + { + TypeSyntax type = SyntaxFactory.ParseTypeName(parameters[0].FullyQualifiedTypeName); + propertyType = SyntaxFactory.QualifiedName(WellKnownSyntax.NameOfCommunityToolkitMvvmInput, SyntaxFactory.GenericName(identifier).WithTypeArgumentList(SyntaxFactory.TypeArgumentList(SyntaxFactory.SingletonSeparatedList(type)))); + } + else + { + propertyType = SyntaxFactory.QualifiedName(WellKnownSyntax.NameOfCommunityToolkitMvvmInput, SyntaxFactory.IdentifierName(identifier)); + } + + return propertyType; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/DependencyPropertyGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/DependencyPropertyGenerator.cs new file mode 100644 index 0000000..4728e21 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/DependencyPropertyGenerator.cs @@ -0,0 +1,242 @@ +// 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.Threading; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory; +using static Snap.Hutao.SourceGeneration.WellKnownSyntax; + +namespace Snap.Hutao.SourceGeneration.Xaml; + +[Generator(LanguageNames.CSharp)] +internal sealed class DependencyPropertyGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownMetadataNames.DependencyPropertyAttributeT, + SyntaxNodeHelper.Is, + Transform) + .Where(static c => c is not null); + + context.RegisterSourceOutput(provider, GenerateWrapper); + } + + private static DependencyPropertyGeneratorContext Transform(GeneratorAttributeSyntaxContext context, CancellationToken token) + { + if (context.TargetSymbol is not INamedTypeSymbol typeSymbol) + { + return default!; + } + + return DependencyPropertyGeneratorContext.Create(typeSymbol); + } + + private static void GenerateWrapper(SourceProductionContext production, DependencyPropertyGeneratorContext context) + { + try + { + Generate(production, context); + } + catch (Exception e) + { + production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString()); + } + } + + private static void Generate(SourceProductionContext production, DependencyPropertyGeneratorContext context) + { + CompilationUnitSyntax syntax = context.Hierarchy + .GetCompilationUnit([.. GenerateMembers(context)]) + .NormalizeWhitespace(); + + production.AddSource(context.Hierarchy.FileNameHint, syntax.ToFullStringWithHeader()); + } + + private static IEnumerable GenerateMembers(DependencyPropertyGeneratorContext context) + { + foreach (AttributeInfo attribute in context.Attributes) + { + if (!attribute.TryGetConstructorArgument(0, out string? name)) + { + continue; + } + + if (!attribute.TryGetTypeArgument(0, out TypeArgumentInfo? propertyType)) + { + continue; + } + + TypeSyntax propertyTypeSyntaxWithoutNullabilityAnnotation = propertyType.GetSyntax(); + TypeSyntax propertyTypeSyntax = propertyTypeSyntaxWithoutNullabilityAnnotation; + + if (!attribute.HasNamedArgument("NotNull", true)) + { + propertyTypeSyntax = NullableType(propertyTypeSyntax); + } + + // Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata) + // RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata) + SeparatedSyntaxList registerArguments = SeparatedList( + [ + Argument(StringLiteralExpression(name)), // name + Argument(TypeOfExpression(propertyTypeSyntaxWithoutNullabilityAnnotation)), // propertyType + Argument(TypeOfExpression(IdentifierName(context.Hierarchy.Hierarchy[0].Name))) // ownerType + ]); + + // PropertyMetadata.Create(object defaultValue) + // PropertyMetadata.Create(object defaultValue, PropertyChangedCallback propertyChangedCallback) + // These two are not supposed to be used because they can be called multiple times and cause issues. + // PropertyMetadata.Create(CreateDefaultValueCallback createDefaultValueCallback) + // PropertyMetadata.Create(CreateDefaultValueCallback createDefaultValueCallback, PropertyChangedCallback propertyChangedCallback) + SeparatedSyntaxList createArguments = SeparatedList(); + if (attribute.TryGetNamedArgument("CreateDefaultValueCallbackName", out string? createDefaultValueCallbackName)) + { + createArguments = createArguments.Add(Argument(InvocationExpression(IdentifierName(createDefaultValueCallbackName)).WithEmptyArgumentList())); + } + else + { + bool hasDefaultValue = attribute.TryGetNamedArgument("DefaultValue", out TypedConstantInfo? defaultValue); + createArguments = createArguments.Add(hasDefaultValue + ? Argument(defaultValue!.GetSyntax()) + : Argument(DefaultExpression(ObjectType))); + } + + if (attribute.TryGetNamedArgument("PropertyChangedCallbackName", out string? propertyChangedCallbackName)) + { + createArguments = createArguments.Add(Argument(IdentifierName(propertyChangedCallbackName))); + } + + registerArguments = registerArguments.Add(Argument(InvocationExpression( + SimpleMemberAccessExpression( + SimpleMemberAccessExpression( + NameOfMicrosoftUIXaml, + IdentifierName("PropertyMetadata")), + IdentifierName("Create"))) + .WithArgumentList(ArgumentList(createArguments)))); + + bool isAttached = attribute.HasNamedArgument("IsAttached", true); + + TypeSyntax dependencyPropertyType = QualifiedName(NameOfMicrosoftUIXaml, IdentifierName("DependencyProperty")); + string propertyName = $"{name}Property"; + + yield return FieldDeclaration(VariableDeclaration(dependencyPropertyType) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(Identifier(propertyName)) + .WithInitializer(EqualsValueClause( + InvocationExpression(SimpleMemberAccessExpression( + SimpleMemberAccessExpression( + NameOfMicrosoftUIXaml, + IdentifierName("DependencyProperty")), + IdentifierName(isAttached ? "RegisterAttached" : "Register"))) + .WithArgumentList(ArgumentList(registerArguments))))))) + .WithModifiers(PrivateStaticReadonlyTokenList); + + if (!isAttached) + { + // Generate a property for non-attached properties + yield return PropertyDeclaration(propertyTypeSyntax, Identifier(name)) + .WithModifiers(PublicTokenList) + .WithIdentifier(Identifier(name)) + .WithAccessorList(AccessorList(List( + [ + GetAccessorDeclaration().WithExpressionBody(ArrowExpressionClause(CastExpression( + propertyTypeSyntaxWithoutNullabilityAnnotation, + InvocationExpression(IdentifierName("GetValue")) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(IdentifierName(propertyName)))))))), + SetAccessorDeclaration().WithExpressionBody(ArrowExpressionClause( + InvocationExpression(IdentifierName("SetValue")) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName(propertyName)), + Argument(IdentifierName("value")) + ]))))) + ]))); + } + else + { + // Generate static methods for attached properties + TypeSyntax targetTypeSyntax; + if (attribute.TryGetNamedArgument("TargetType", out TypedConstantInfo? targetType) && + targetType is TypedConstantInfo.Type type) + { + targetTypeSyntax = ParseName(type.FullyQualifiedTypeName); + } + else + { + targetTypeSyntax = QualifiedName(NameOfMicrosoftUIXaml, IdentifierName("DependencyObject")); + } + + // Generate static methods for attached properties + yield return MethodDeclaration(propertyTypeSyntax, Identifier($"Get{name}")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(targetTypeSyntax, Identifier("obj")) + ]))) + .WithBody(Block(List( + [ + ExpressionStatement(ArgumentNullExceptionThrowIfNull(IdentifierName("obj"))), + + ReturnStatement(CastExpression( + propertyTypeSyntax, + InvocationExpression( + SimpleMemberAccessExpression( + IdentifierName("obj"), + IdentifierName("GetValue"))) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(IdentifierName(propertyName))))))) + ]))); + + yield return MethodDeclaration(VoidType, Identifier($"Set{name}")) + .WithModifiers(PublicStaticTokenList) + .WithParameterList(ParameterList(SeparatedList( + [ + Parameter(targetTypeSyntax, Identifier("obj")), + Parameter(propertyTypeSyntax, Identifier("value")) + ]))) + .WithBody(Block(List( + [ + ExpressionStatement(ArgumentNullExceptionThrowIfNull(IdentifierName("obj"))), + + ExpressionStatement( + InvocationExpression( + SimpleMemberAccessExpression( + IdentifierName("obj"), + IdentifierName("SetValue"))) + .WithArgumentList(ArgumentList(SeparatedList( + [ + Argument(IdentifierName(propertyName)), + Argument(IdentifierName("value")) + ])))) + ]))); + } + } + } + + private sealed record DependencyPropertyGeneratorContext + { + public required HierarchyInfo Hierarchy { get; init; } + + public required EquatableArray Attributes { get; init; } + + public static DependencyPropertyGeneratorContext Create(INamedTypeSymbol typeSymbol) + { + return new() + { + Hierarchy = HierarchyInfo.Create(typeSymbol), + Attributes = ImmutableArray.CreateRange(typeSymbol.GetAttributes(), AttributeInfo.Create) + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/PropertyValuesProviderGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/PropertyValuesProviderGenerator.cs new file mode 100644 index 0000000..08b99ce --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/PropertyValuesProviderGenerator.cs @@ -0,0 +1,111 @@ +// 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; + +namespace Snap.Hutao.SourceGeneration.Xaml; + +[Generator(LanguageNames.CSharp)] +internal sealed class PropertyValuesProviderGenerator : IIncrementalGenerator +{ + public const string InterfaceMetadataName = "Snap.Hutao.UI.Xaml.Data.IPropertyValuesProvider"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider commands = context.SyntaxProvider + .CreateSyntaxProvider(SyntaxNodeHelper.TypeHasBaseType, InheritedType) + .Where(static c => c is not null); + + context.RegisterSourceOutput(commands, GenerateWrapper); + } + + private static PropertyValuesProviderGeneratorContext InheritedType(GeneratorSyntaxContext context, CancellationToken token) + { + if (context.SemanticModel.GetDeclaredSymbol(context.Node) is INamedTypeSymbol typeSymbol) + { + if (typeSymbol.Interfaces.Any(static symbol => symbol.HasFullyQualifiedMetadataName(InterfaceMetadataName))) + { + return PropertyValuesProviderGeneratorContext.Create(typeSymbol); + } + } + + return default!; + } + + private static void GenerateWrapper(SourceProductionContext production, PropertyValuesProviderGeneratorContext context) + { + try + { + Generate(production, context); + } + catch (Exception e) + { + production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString()); + } + } + + private static void Generate(SourceProductionContext production, PropertyValuesProviderGeneratorContext context) + { + CompilationUnitSyntax syntax = context.Hierarchy.GetCompilationUnit( + [ + MethodDeclaration(NullableObjectType, Identifier("GetPropertyValue")) + .WithModifiers(PublicTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(StringType, Identifier("propertyName"))))) + .WithBody(Block(SingletonList( + ReturnStatement(SwitchExpression(IdentifierName("propertyName")) + .WithArms(SeparatedList(GenerateSwitchExpressionArms(context))))))) + ]) + .NormalizeWhitespace(); + + production.AddSource(context.Hierarchy.FileNameHint, syntax.ToFullStringWithHeader()); + } + + private static IEnumerable GenerateSwitchExpressionArms(PropertyValuesProviderGeneratorContext context) + { + foreach (PropertyInfo property in context.Properties) + { + IdentifierNameSyntax propertyName = IdentifierName(property.Name); + + // nameof(${propertyName}) => ${propertyName} + yield return SwitchExpressionArm( + ConstantPattern(NameOfExpression(propertyName)), + propertyName); + } + + // _ => default + yield return SwitchExpressionArm(DiscardPattern(), DefaultLiteralExpression); + } + + private sealed record PropertyValuesProviderGeneratorContext + { + public required HierarchyInfo Hierarchy { get; init; } + + public required EquatableArray Properties { get; init; } + + public static PropertyValuesProviderGeneratorContext Create(INamedTypeSymbol typeSymbol) + { + return new() + { + Hierarchy = HierarchyInfo.Create(typeSymbol), + Properties = typeSymbol + .GetMembers() + .OfType() + .Where(static prop => prop.DeclaredAccessibility is Accessibility.Public) + .Select(PropertyInfo.Create) + .ToImmutableArray(), + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/XamlUnloadObjectOverrideGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/XamlUnloadObjectOverrideGenerator.cs new file mode 100644 index 0000000..7018f40 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Xaml/XamlUnloadObjectOverrideGenerator.cs @@ -0,0 +1,84 @@ +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.Threading; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory; + +namespace Snap.Hutao.SourceGeneration.Xaml; + +[Generator] +internal class XamlUnloadObjectOverrideGenerator : IIncrementalGenerator +{ + private const string ClassMetadataName = "Snap.Hutao.UI.Xaml.Control.ScopedPage"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider inheritClasses = context.SyntaxProvider + .CreateSyntaxProvider(SyntaxNodeHelper.TypeHasBaseType, InheritedType) + .Where(static c => c is not null) + .Distinct(); + + context.RegisterSourceOutput(inheritClasses, GenerateWrapper); + } + + private static XamlUnloadObjectOverrideGeneratorContext InheritedType(GeneratorSyntaxContext context, CancellationToken token) + { + if (context.SemanticModel.GetDeclaredSymbol(context.Node) is INamedTypeSymbol typeSymbol) + { + if (typeSymbol.BaseType?.HasFullyQualifiedMetadataName(ClassMetadataName) is true) + { + return XamlUnloadObjectOverrideGeneratorContext.Create(typeSymbol); + } + } + + return default!; + } + + private static void GenerateWrapper(SourceProductionContext production, XamlUnloadObjectOverrideGeneratorContext context) + { + try + { + Generate(production, context); + } + catch (Exception e) + { + production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString()); + } + } + + private static void Generate(SourceProductionContext production, XamlUnloadObjectOverrideGeneratorContext context) + { + CompilationUnitSyntax syntax = context.Hierarchy.GetCompilationUnit( + [ + MethodDeclaration(VoidType, Identifier("UnloadObjectOverride")) + .WithModifiers(PublicOverrideTokenList) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(ParseTypeName("global::Microsoft.UI.Xaml.DependencyObject"), Identifier("unloadableObject"))))) + .WithBody(Block(SingletonList( + ExpressionStatement( + InvocationExpression(IdentifierName("UnloadObject")) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(IdentifierName("unloadableObject"))))))))), + ]) + .NormalizeWhitespace(); + + production.AddSource(context.Hierarchy.FileNameHint, syntax.ToFullStringWithHeader()); + } + + private sealed record XamlUnloadObjectOverrideGeneratorContext + { + public required HierarchyInfo Hierarchy { get; init; } + + public static XamlUnloadObjectOverrideGeneratorContext Create(INamedTypeSymbol symbol) + { + return new() + { + Hierarchy = HierarchyInfo.Create(symbol), + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs new file mode 100644 index 0000000..05079d7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public class CollectionsMarshalTest +{ + [TestMethod] + public void DictionaryMarshalGetValueRefOrNullRefIsNullRef() + { + Dictionary dictionaryValueKeyRefValue = []; + Dictionary dictionaryValueKeyValueValue = []; + Dictionary dictionaryRefKeyValueValue = []; + Dictionary dictionaryRefKeyRefValue = []; + + Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U))); + Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U))); + Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyValueValue, "no such key"))); + Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyRefValue, "no such key"))); + } + + [TestMethod] + public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault() + { + Dictionary dictionaryValueKeyRefValue = []; + Dictionary dictionaryValueKeyValueValue = []; + Dictionary dictionaryRefKeyValueValue = []; + Dictionary dictionaryRefKeyRefValue = []; + + Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default); + Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default); + Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyValueValue, "no such key", out _) == default); + Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyRefValue, "no such key", out _) == default); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/GCHandleTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/GCHandleTest.cs new file mode 100644 index 0000000..f6f1185 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/GCHandleTest.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public class GCHandleTest +{ + [TestMethod] + public unsafe void TypedGCHandleIsSameSizeAsNInt() + { + Assert.AreEqual(sizeof(GCHandle), sizeof(nint)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/HttpClientTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/HttpClientTest.cs new file mode 100644 index 0000000..37abcd4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/HttpClientTest.cs @@ -0,0 +1,31 @@ +using System.Net.Http; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public sealed class HttpClientTest +{ + [TestMethod] + public void RedirectionHeaderTest() + { + HttpClientHandler handler = new() + { + UseCookies = false, + AllowAutoRedirect = false, + }; + + using (handler) + { + using (HttpClient httpClient = new(handler)) + { + using (HttpRequestMessage request = new(HttpMethod.Get, "https://api.snapgenshin.com/patch/hutao/download")) + { + using (HttpResponseMessage response = httpClient.Send(request)) + { + _ = response; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ImmutableCollectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ImmutableCollectionTest.cs new file mode 100644 index 0000000..018c88a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ImmutableCollectionTest.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public class ImmutableCollectionTest +{ + [TestMethod] + public void ImmutableArrayUnsafeRefWrite() + { + ImmutableArray array = [1, 2, 3, 4, 5, 6, 7]; + Unsafe.AsRef(in array.AsSpan()[3]) = 8; + Assert.AreEqual(8, array[3]); + } + + [TestMethod] + public void ArrayImplementsIListT() + { + int[] array = [1, 2, 3, 4, 5, 6, 7]; + Assert.IsTrue(array is IList); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs new file mode 100644 index 0000000..df6ba11 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs @@ -0,0 +1,369 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public sealed class JsonSerializeTest +{ + private readonly JsonSerializerOptions AlowStringNumberOptions = new() + { + NumberHandling = JsonNumberHandling.AllowReadingFromString, + }; + + private const string SampleObjectJson = """ + { + "A" :1 + } + """; + + private const string SampleEmptyStringObjectJson = """ + { + "A" : "" + } + """; + + private const string SampleNumberKeyDictionaryJson = """ + { + "111" : "12", + "222" : "34" + } + """; + + [TestMethod] + public void DelegatePropertyCanSerialize() + { + SampleDelegatePropertyClass sample = JsonSerializer.Deserialize(SampleObjectJson)!; + Assert.AreEqual(1, sample.B); + } + + [TestMethod] + public void EmptyStringCannotSerializeAsNumber() + { + SampleStringReadWriteNumberPropertyClass sample = default!; + Assert.Throws(() => + { + sample = JsonSerializer.Deserialize(SampleEmptyStringObjectJson)!; + Assert.AreEqual(0, sample.A); + }); + } + + [TestMethod] + public void EmptyStringCanSerializeAsUri() + { + SampleEmptyUriClass sample = JsonSerializer.Deserialize(SampleEmptyStringObjectJson)!; + Uri.TryCreate("", UriKind.RelativeOrAbsolute, out Uri? value); + Console.WriteLine(value); + Assert.AreEqual(value, sample.A); + } + + [TestMethod] + public void NumberStringKeyCanSerializeAsKey() + { + Dictionary sample = JsonSerializer.Deserialize>(SampleNumberKeyDictionaryJson, AlowStringNumberOptions)!; + Assert.AreEqual("12", sample[111]); + } + + [TestMethod] + public void ByteArraySerializeAsBase64() + { + SampleByteArrayPropertyClass sample = new() + { + Array = [1, 2, 3, 4, 5], + }; + + string result = JsonSerializer.Serialize(sample); + Assert.AreEqual("""{"Array":"AQIDBAU="}""", result); + } + + [TestMethod] + public void InterfaceDefaultMethodCanSerializeActualInstanceMember() + { + ISampleInterface sample = new SampleClassImplementedInterface() + { + A = 1, + B = 2, + }; + + string result = sample.ToJson(); + Console.WriteLine(result); + Assert.AreEqual("""{"A":1,"B":2}""", result); + } + + [TestMethod] + public void LowercaseStringCanDeserializeAsEnum() + { + string source = """ + { + "Value": "a" + } + """; + + SampleClassHoldEnum sample = JsonSerializer.Deserialize(source)!; + Assert.AreEqual(SampleEnum.A, sample.Value); + } + + [TestMethod] + public void InitPropertyCanDeserialize() + { + string source = """ + { + "Value": "A" + } + """; + + SampleClassHoldEnumInitOnly sample = JsonSerializer.Deserialize(source)!; + Assert.AreEqual(SampleEnum.A, sample.Value); + } + + [TestMethod] + public void NewEmptyObjectSerializeAsEmptyObject() + { + object sample = new(); + string result = JsonSerializer.Serialize(sample); + Assert.AreEqual("{}", result); + } + + [TestMethod] + public void StructCanDeserialize() + { + SampleStruct sample = JsonSerializer.Deserialize(SampleObjectJson); + Assert.AreEqual(1, sample.A); + } + + [TestMethod] + public void DerivedTypeTest() + { + Parent p = new Child() + { + A = 1, + B = 2, + C = 3, + }; + + Dictionary dict = new() + { + ["key1"] = p, + }; + +#pragma warning disable CA1869 + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + HandleDerivedType, + } + } + }; +#pragma warning restore CA1869 + + string result = JsonSerializer.Serialize(dict, options); + Assert.AreEqual("""{"key1":{"$type":"Child","C":3,"B":2,"A":1}}""", result); + } + + [TestMethod] + public void HexConvert() + { + string json = """ + { + "Number": "0x1A2B3C", + "Number2": 1715005 + } + """; + + HexTest test = JsonSerializer.Deserialize(json)!; + Assert.AreEqual(0x1A2B3C, test.Number); + Assert.AreEqual(0x1A2B3D, test.Number2); + } + + private void HandleDerivedType(JsonTypeInfo info) + { + Type? current = info.Type; + HashSet jsonDerivedTypes = info.PolymorphismOptions is null ? [] : [.. info.PolymorphismOptions.DerivedTypes]; + + while (true) + { + if (current is null || current.BaseType is null) + { + break; + } + + foreach (CustomAttributeData attributeData in current.BaseType.CustomAttributes) + { + if (attributeData.AttributeType != typeof(JsonDerivedTypeAttribute)) + { + continue; + } + + if (attributeData.ConstructorArguments[0].Value is not Type derivedType) + { + continue; + } + + if (!derivedType.IsAssignableTo(info.Type)) + { + continue; + } + + if (derivedType == info.Type) + { + continue; + } + + switch (attributeData.ConstructorArguments[1].Value) + { + case string name: + jsonDerivedTypes.Add(new JsonDerivedType(derivedType, name)); + break; + case int value: + jsonDerivedTypes.Add(new JsonDerivedType(derivedType, value)); + break; + default: + jsonDerivedTypes.Add(new JsonDerivedType(derivedType)); + break; + } + } + + current = current.BaseType; + } + + if (jsonDerivedTypes.Count <= 0) + { + return; + } + + info.PolymorphismOptions ??= new(); + foreach (JsonDerivedType derivedType in jsonDerivedTypes) + { + info.PolymorphismOptions.DerivedTypes.Add(derivedType); + } + } + + private sealed class SampleDelegatePropertyClass + { + public int A { get => B; set => B = value; } + public int B { get; set; } + } + + private sealed class SampleStringReadWriteNumberPropertyClass + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public int A { get; set; } + } + + private sealed class SampleEmptyUriClass + { + public Uri A { get; set; } = default!; + } + + private sealed class SampleByteArrayPropertyClass + { + public byte[]? Array { get; set; } + } + + private sealed class SampleClassImplementedInterface : ISampleInterface + { + public int A { get; set; } + + public int B { get; set; } + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + private enum SampleEnum + { + A, + B, + } + + private sealed class SampleClassHoldEnum + { + public SampleEnum Value { get; set; } + } + + private sealed class SampleClassHoldEnumInitOnly + { + public SampleEnum Value { get; init; } + } + + [JsonDerivedType(typeof(SampleClassImplementedInterface))] + private interface ISampleInterface + { + int A { get; set; } + + string ToJson() + { + return JsonSerializer.Serialize(this); + } + } + + [SuppressMessage("", "CS0649")] + private struct SampleStruct + { + [JsonInclude] + public int A; + } + + [JsonDerivedType(typeof(Parent), nameof(Parent))] + [JsonDerivedType(typeof(Child), nameof(Child))] + private class GrandParent + { + public int A { get; set; } + } + + private class Parent : GrandParent + { + public int B { get; set; } + } + + private class Child : Parent + { + public int C { get; set; } + } + + private class HexTest + { + [JsonConverter(typeof(HexNumberConverter))] + public uint Number { get; set; } + + [JsonConverter(typeof(HexNumberConverter))] + public uint Number2 { get; set; } + } + + internal sealed class HexNumberConverter : JsonConverter + where T : struct, INumberBase + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.String) + { + if ((reader.ValueSpan.StartsWith("0x"u8) || reader.ValueSpan.StartsWith("0X"u8)) && + T.TryParse(reader.ValueSpan[2..], NumberStyles.HexNumber, default, out T hex)) + { + return hex; + } + } + else if (reader.TokenType is JsonTokenType.Number && reader.TryGetInt64(out long value)) + { + return T.Parse(reader.ValueSpan, CultureInfo.CurrentCulture); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue($"0x{value:X}"); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs new file mode 100644 index 0000000..61a419b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public sealed class LinqTest +{ + [TestMethod] + public void LinqOrderByWithWrapperStructThrow() + { + List list = [1, 5, 2, 6, 3, 7, 4, 8]; + string result = default!; + Assert.Throws(() => + { + result = string.Join(", ", list.OrderBy(i => i).Select(i => i.Value)); + }); + + Console.WriteLine(result); + } + + [TestMethod] + public void SequenceAndRangeProduceSameRusult() + { + IEnumerable rangeResult = Enumerable.Range(4, 5); + IEnumerable sequenceResult = Enumerable.Sequence(4U, 8U, 1U).Select(x => (int)x); + + CollectionAssert.AreEqual(rangeResult.ToList(), sequenceResult.ToList()); + } + + private readonly struct MyUInt32 + { + public readonly uint Value; + + public MyUInt32(uint value) + { + Value = value; + } + + public static implicit operator MyUInt32(uint value) + { + return new(value); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ListTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ListTest.cs new file mode 100644 index 0000000..cf96881 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ListTest.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public sealed class ListTest +{ + [TestMethod] + public void IndexOfNullIsNegativeOne() + { + List list = [new()]; + Assert.AreEqual(-1, list.IndexOf(default!)); + } + + [TestMethod] + public void StructElementMultipleIndexOfTest() + { + List list = [1, 1, 1, 1]; + Assert.AreEqual(0, list.IndexOf(1)); + Assert.IsTrue(Equals(list[0], list[1])); + Assert.IsFalse(ReferenceEquals(list[0], list[0])); + Assert.IsFalse(ReferenceEquals(list[0], list[1])); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/PartialPropertyTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/PartialPropertyTest.cs new file mode 100644 index 0000000..397193b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/PartialPropertyTest.cs @@ -0,0 +1,27 @@ +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public class PartialPropertyTest +{ + [TestMethod] + public void Test() + { + PartialPropertyClass partialPropertyClass = new(); + Assert.AreEqual("Test", partialPropertyClass.Property); + } + + private partial class PartialPropertyClass + { + public partial string Property { get; } + } + + partial class PartialPropertyClass + { + public PartialPropertyClass() + { + Property = "Test"; + } + + public partial string Property { get => field; } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/TypeReflectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/TypeReflectionTest.cs new file mode 100644 index 0000000..dc3a485 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/TypeReflectionTest.cs @@ -0,0 +1,19 @@ +using System; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public sealed class TypeReflectionTest +{ + [TestMethod] + public void TypeCodeOfEnumIsUserlyingTypeTypeCode() + { + Assert.AreEqual(Type.GetTypeCode(typeof(TestEnum)), TypeCode.Int32); + } + + private enum TestEnum + { + A, + B, + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/UnsafeAccessorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/UnsafeAccessorTest.cs new file mode 100644 index 0000000..d5fa497 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/UnsafeAccessorTest.cs @@ -0,0 +1,50 @@ +using System; +using System.Net; +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public class UnsafeAccessorTest +{ + [TestMethod] + public void UnsafeAccessorCanGetInterfaceProperty() + { + TestClass test = new(); + int value = InternalGetInterfaceProperty(test); + Assert.AreEqual(3, value); + + IWebProxy proxy = ConstructSystemProxy(null); + } + + [TestMethod] + public void BehaviorTest() + { + DateTimeOffset dto = new(2000, 2, 3, 4, 5, 6, TimeSpan.FromHours(7)); + Assert.AreEqual(420, RefValueGetFieldRef(ref dto)); + Assert.AreEqual(420, RefValueGetFieldRefReadonly(ref dto)); + } + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")] + private static extern int InternalGetInterfaceProperty(ITestInterface instance); + + // private readonly int _offsetMinutes; + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_offsetMinutes")] + private static extern ref int RefValueGetFieldRef(ref DateTimeOffset dto); + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_offsetMinutes")] + private static extern ref readonly int RefValueGetFieldRefReadonly(ref DateTimeOffset dto); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConstructSystemProxy")] + private extern static IWebProxy ConstructSystemProxy([UnsafeAccessorType("System.Net.Http.SystemProxyInfo, System.Net.Http")] object? c); + + internal interface ITestInterface + { + internal int TestProperty { get; } + } + + internal sealed class TestClass : ITestInterface + { + public int TestProperty { get; } = 3; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs b/src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs new file mode 100644 index 0000000..ab67c7e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryAdlTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryAdlTest.cs new file mode 100644 index 0000000..37e9b37 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryAdlTest.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.NetworkInformation; +using System.Text; +using System.Security.Cryptography; + +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Test.IncomingFeature; + +[TestClass] +public class GameRegistryAdlTest +{ + private static readonly byte[] EncIv = [0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF]; + + // MiHoYo.SDK.DeviceInfoManager$$GetMacAddress + private static string GetMacAddress() + { + string address = ""; + foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + address = networkInterface.GetPhysicalAddress().ToString(); + if (networkInterface.Description is "en0" || address != "") + { + return address; + } + } + + return address; + } + + // MiHoYo.SDK.DataStorageManager$$GetEncodeValue + private static string GetEncodeValue() + { + string mac = GetMacAddress(); + return mac == "" || mac.Length < 8 ? "FFFFFFFFFFFF" : mac[..8]; + } + + // MiHoYo.SDK.DataStorageManager$$EncodeString + public static string EncodeString(string str) + { + using DES des = DES.Create(); + des.Key = Encoding.UTF8.GetBytes(GetEncodeValue()); + return Convert.ToBase64String(des.EncryptCbc(Encoding.UTF8.GetBytes(str), EncIv)); + } + + // MiHoYo.SDK.DataStorageManager$$DecodeString + public static string DecodeString(string str) + { + using DES des = DES.Create(); + des.Key = Encoding.UTF8.GetBytes(GetEncodeValue()); + return Encoding.UTF8.GetString(des.DecryptCbc(Convert.FromBase64String(str), EncIv)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryContentTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryContentTest.cs new file mode 100644 index 0000000..46b9669 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryContentTest.cs @@ -0,0 +1,68 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Text.Json; + +namespace Snap.Hutao.Test.IncomingFeature; + +[TestClass] +public class GameRegistryContentTest +{ + private static readonly JsonSerializerOptions RegistryContentSerializerOptions = new() + { + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + [TestMethod] + [SupportedOSPlatform("windows")] + public void GetRegistryContent() + { + TestGetRegistryContent(@"Software\miHoYo\原神"); + TestGetRegistryContent(@"Software\miHoYo\Genshin Impact"); + } + + [SupportedOSPlatform("windows")] + private static void TestGetRegistryContent(string subkey) + { + using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) + { + RegistryKey? gameKey = key.OpenSubKey(subkey); + Assert.IsNotNull(gameKey); + + Dictionary data = []; + foreach (string valueName in gameKey.GetValueNames()) + { + data[valueName] = gameKey.GetValueKind(valueName) switch + { + RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!, + RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!), + RegistryValueKind.String => (string)gameKey.GetValue(valueName)!, + _ => throw new ArgumentException($"Unsupported type: {gameKey.GetValueKind(valueName)}"), + }; + } + + Console.WriteLine($"Subkey: {subkey}"); + Console.WriteLine(JsonSerializer.Serialize(data, RegistryContentSerializerOptions)); + } + } + + private static unsafe object GetStringOrObject(byte[] bytes) + { + fixed (byte* pByte = bytes) + { + ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte); + string temp = Encoding.UTF8.GetString(span); + + if (temp.AsSpan()[0] is '{' or '[') + { + return JsonSerializer.Deserialize(temp); + } + + return temp; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs new file mode 100644 index 0000000..71492f4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs @@ -0,0 +1,241 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; + +namespace Snap.Hutao.Test.IncomingFeature; + +[TestClass] +public sealed class GeniusInvokationDecoding +{ + public TestContext? TestContext { get; set; } + + /// + /// https://www.bilibili.com/video/av278125720 + /// + [TestMethod] + public unsafe void GeniusInvokationShareCodeDecoding() + { + // 51 bytes obfuscated data + byte[] bytes = Convert.FromBase64String("BCHBwxQNAYERyVANCJGBynkOCZER2pgOCrFx8poQChGR9bYQDEGB9rkQDFKRD7oRDeEB"); + + // --------------------------------------------- + // | Data | Caesar Cipher Key | + // |----------|-------------------| + // | 50 Bytes | 1 Byte | + // --------------------------------------------- + // Data: + // 00000100 00100001 11000001 11000011 00010100 + // 00001101 00000001 10000001 00010001 11001001 + // 01010000 00001101 00001000 10010001 10000001 + // 11001010 01111001 00001110 00001001 10010001 + // 00010001 11011010 10011000 00001110 00001010 + // 10110001 01110001 11110010 10011010 00010000 + // 00001010 00010001 10010001 11110101 10110110 + // 00010000 00001100 01000001 10000001 11110110 + // 10111001 00010000 00001100 01010010 10010001 + // 00001111 10111010 00010001 00001101 11100001 + // --------------------------------------------- + // Caesar Cipher Key: + // 00000001 + // --------------------------------------------- + fixed (byte* ptr = bytes) + { + // Reinterpret as 50 byte actual data and 1 deobfuscate key byte + EncryptedDataAndKey* data = (EncryptedDataAndKey*)ptr; + byte* dataPtr = data->Data; + + // ---------------------------------------------------------- + // | First | Second | Padding | + // |-----------|----------|---------| + // | 25 Bytes | 25 Bytes | 1 Byte | + // ---------------------------------------------------------- + // We are doing two things here: + // 1. Retrieve actual data by subtracting key + // 2. Store data into two halves by alternating between them + // ---------------------------------------------------------- + // What we will get after this step: + // ---------------------------------------------------------- + // First: + // 00000011 11000000 00010011 00000000 00010000 + // 01001111 00000111 10000000 01111000 00001000 + // 00010000 10010111 00001001 01110000 10011001 + // 00001001 10010000 10110101 00001011 10000000 + // 10111000 00001011 10010000 10111001 00001100 + // ---------------------------------------------------------- + // Second: + // 00100000 11000010 00001100 10000000 11001000 + // 00001100 10010000 11001001 00001101 10010000 + // 11011001 00001101 10110000 11110001 00001111 + // 00010000 11110100 00001111 01000000 11110101 + // 00001111 01010001 00001110 00010000 11100000 + // ---------------------------------------------------------- + RearrangeBuffer rearranged = default; + byte* pFirst = rearranged.First; + byte* pSecond = rearranged.Second; + for (int i = 0; i < 50; i++) + { + // Determine which half are we going to insert + byte** ppTarget = i % 2 == 0 ? &pFirst : &pSecond; + + // (actual data = data - key) and store it directly to the target half + **ppTarget = unchecked((byte)(dataPtr[i] - data->Key)); + + (*ppTarget)++; + } + + // Prepare decoded data result storage + DecryptedData decoded = default; + ushort* pDecoded = decoded.Data; + + // ---------------------------------------------------------- + // | Data | + // |----------| x 17 = 51 Bytes + // | 3 Bytes | + // ---------------------------------------------------------- + // Grouping each 3 bytes and read out as 2 ushort with + // 12 bits each (Big Endian) + // ---------------------------------------------------------- + // 00000011 1100·0000 00010011| + // 00000000 0001·0000 01001111| + // 00000111 1000·0000 01111000| + // 00001000 0001·0000 10010111| + // 00001001 0111·0000 10011001| + // 00001001 1001·0000 10110101| + // 00001011 1000·0000 10111000| + // 00001011 1001·0000 10111001| + // 00001100 0010·0000 11000010| + // 00001100 1000·0000 11001000| + // 00001100 1001·0000 11001001| + // 00001101 1001·0000 11011001| + // 00001101 1011·0000 11110001| + // 00001111 0001·0000 11110100| + // 00001111 0100·0000 11110101| + // 00001111 0101·0001 00001110| + // 00010000 1110·0000 -padding|[padding32] + // ---------------------------------------------------------- + // reinterpret as DecodeGroupingHelper for each 3 bytes + DecodeGroupingHelper* pGroup = (DecodeGroupingHelper*)&rearranged; + for (int i = 0; i < 17; i++) + { + (ushort first, ushort second) = pGroup->GetData(); + + *pDecoded = first; + *(pDecoded + 1) = second; + + pDecoded += 2; + pGroup++; + } + + // Now we get + // 60, 19, 1, + // 79,120,120, + // 129,151,151, + // 153,153,181, + // 184,184,185, + // 185,194,194, + // 200,200,201, + // 201,217,217, + // 219,241,241, + // 244,244,245, + // 245,270,270, + StringBuilder stringBuilder = new(); + for (int i = 0; i < 33; i++) + { + stringBuilder + .AppendFormat(CultureInfo.InvariantCulture, "{0,3}", decoded.Data[i]) + .Append(','); + + if (i % 11 == 10) + { + stringBuilder.Append('\n'); + } + } + + TestContext?.WriteLine(stringBuilder.ToString(0, stringBuilder.Length - 1)); + + ushort[] resultArray = new ushort[33]; + Span result = new((ushort*)&decoded, 33); + result.CopyTo(resultArray); + + ushort[] testKnownResult = + [ + 060, + 019, + 001, + 079, + 120, + 120, + 129, + 151, + 151, + 153, + 153, + 181, + 184, + 184, + 185, + 185, + 194, + 194, + 200, + 200, + 201, + 201, + 217, + 217, + 219, + 241, + 241, + 244, + 244, + 245, + 245, + 270, + 270, + ]; + + CollectionAssert.AreEqual(resultArray, testKnownResult); + } + } + + [SuppressMessage("", "CS0649")] + private struct EncryptedDataAndKey + { + public unsafe fixed byte Data[50]; + public byte Key; + } + + private struct RearrangeBuffer + { + public unsafe fixed byte First[25]; + public unsafe fixed byte Second[25]; + + // Make it 51 bytes + // allow to be group as 17 DecodeGroupingHelper later + public byte padding; + + // prevent accidently int32 cast access violation + public byte paddingTo32; + } + + private struct DecodeGroupingHelper + { + public unsafe fixed byte Data[3]; + + public unsafe (ushort First, ushort Second) GetData() + { + fixed (byte* ptr = Data) + { + uint value = BinaryPrimitives.ReverseEndianness((*(uint*)ptr) & 0x00FFFFFF) >> 8; // keep low 24 bits only + return ((ushort)((value >> 12) & 0x0FFF), (ushort)(value & 0x0FFF)); + } + } + } + + private struct DecryptedData + { + public unsafe fixed ushort Data[33]; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs new file mode 100644 index 0000000..b958e85 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs @@ -0,0 +1,60 @@ +using System; + +namespace Snap.Hutao.Test.IncomingFeature; + +[TestClass] +public class SpiralAbyssScheduleIdTest +{ + private static readonly TimeSpan Utc8 = TimeSpan.FromHours(8); + private static readonly DateTimeOffset AcrobaticsBattleIntroducedTime = new(2024, 7, 1, 4, 0, 0, Utc8); + + [TestMethod] + public void Test() + { + Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期"); + + // 2020-07-01 04:00:00 为第 1 期 + // 2024-06-16 04:00:00 为第 96 期 + // 2024-07-01 04:00:00 为第 97 期 + // 2024-07-16 04:00:00 为第 98 期 + // 2024-08-01 04:00:00 为第 99 期 + Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2020, 07, 01, 4, 0, 0, Utc8))} 期"); + Console.WriteLine($"2024-06-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 06, 16, 4, 0, 0, Utc8))} 期"); + Console.WriteLine($"2024-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 01, 4, 0, 0, Utc8))} 期"); + Console.WriteLine($"2024-07-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 16, 4, 0, 0, Utc8))} 期"); + Console.WriteLine($"2024-08-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 01, 4, 0, 0, Utc8))} 期"); + Console.WriteLine($"2024-08-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 16, 4, 0, 0, Utc8))} 期"); + Console.WriteLine($"2024-09-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 09, 01, 4, 0, 0, Utc8))} 期"); + } + + public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset) + { + // Force time in UTC+08 + dateTimeOffset = dateTimeOffset.ToOffset(Utc8); + + ((int year, int mouth, int day), (int hour, _), _) = dateTimeOffset; + + // 2020-07-01 04:00:00 为第 1 期 + int periodNum = (((year - 2020) * 12) + (mouth - 6)) * 2; + + // 上半月:1-15 日, 以及 16 日 00:00-04:00 + if (day < 16 || (day == 16 && hour < 4)) + { + periodNum--; + } + + // 上个月:1 日 00:00-04:00 + if (day is 1 && hour < 4) + { + periodNum--; + } + + if (dateTimeOffset >= AcrobaticsBattleIntroducedTime) + { + // 当超过 96 期时,每一个月一期 + periodNum = (4 * 12 * 2) + ((periodNum - (4 * 12 * 2)) / 2); + } + + return periodNum; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnUsedResourceStringTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnUsedResourceStringTest.cs new file mode 100644 index 0000000..d2acb42 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnUsedResourceStringTest.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Snap.Hutao.Test.IncomingFeature; + +[TestClass] +public partial class UnUsedResourceStringTest +{ + [TestMethod] + public void GetAllUnUsedResourceStrings() + { + string path = AppContext.BaseDirectory; + for(int i = 0; i < 5; i++) + { + path = Path.GetDirectoryName(path)!; + } + + string resxPath = Path.Combine(path, @"Snap.Hutao\Resource\Localization\SH.resx"); + MatchCollection matches = DataRegex.Matches(File.ReadAllText(resxPath)); + HashSet strings = matches.Select(m => m.Groups[1].Value).ToHashSet(); + + + foreach (ref string file in Directory.GetFiles(path,"*", SearchOption.AllDirectories).AsSpan()) + { + if (Path.GetExtension(file) is not (".cs" or ".xaml")) + { + continue; + } + + if (file.Contains("Snap.Hutao.Test", StringComparison.OrdinalIgnoreCase) || + file.Contains("Snap.Hutao\\obj", StringComparison.OrdinalIgnoreCase) || + file.Contains("Snap.Hutao\\bin", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string content = File.ReadAllText(file); + foreach (string str in strings) + { + if (content.Contains(str, StringComparison.Ordinal)) + { + strings.Remove(str); + } + } + } + + foreach (string str in strings) + { + if (str.StartsWith("Server", StringComparison.Ordinal)) + { + continue; + } + + Console.WriteLine(str); + } + } + + [GeneratedRegex(""" + data name="(.*?)" xml:space="preserve" + """)] + private partial Regex DataRegex { get; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnlockerIslandFunctionOffsetTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnlockerIslandFunctionOffsetTest.cs new file mode 100644 index 0000000..e3a519f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnlockerIslandFunctionOffsetTest.cs @@ -0,0 +1,112 @@ +using System; +using System.Text.Json; + +namespace Snap.Hutao.Test.IncomingFeature; + +[TestClass] +public class UnlockerIslandFunctionOffsetTest +{ + private static readonly JsonSerializerOptions Options = new() + { + WriteIndented = true, + }; + + [TestMethod] + public void GenerateJson() + { + // MickeyWonderMethod: + // public static byte[] AnonymousMethod43(int nType) -> jmp xxxxxxxx -> another xref to xxxxxxxx + // MickeyWonderMethodPartner: + // public static string get_unityVersion() -> jmp + // MickeyWonderMethodPartner2: + // "4C 8B 05 ?? ?? ?? ?? 4C 89 F1 48 89 FA E8 ?? ?? ?? ?? 90 48 83 C4 28 5B 5F 5E 41 5E C3" + // SetFieldOfView: + // Camera: public void set_fieldOfView(float value) -> jmp xxxxxxxx + // SetTargetFrameRate: + // public static void set_targetFrameRate(int value) -> jmp xxxxxxxxx (to the end) + // SetEnableFogRendering: + // public static void set_enableFogRendering(bool value) -> jmp xxxxxxxxx (to the end) + // OpenTeam: OpenTeamPageAccordingly: CheckCanEnter GOD HELP US + UnlockerIslandConfigurationWrapper wrapper = new() + { + Chinese = new() + { + MickeyWonderMethod = 135331312, + MickeyWonderMethodPartner = 4622768, + MickeyWonderMethodPartner2 = 109471280, + SetFieldOfView = 15436752, + SetEnableFogRendering = 276053264, + SetTargetFrameRate = 276586352, + OpenTeam = 102865760, + OpenTeamPageAccordingly = 102804496, + CheckCanEnter = 123456848, + SetupQuestBanner = 0x0D92B560, + FindGameObject = 0x107B3AE0, + SetActive = 0x107B3840, + EventCameraMove = 0x08A8A760, + ShowOneDamageTextEx = 0x0A8E4B10, + SwitchInputDeviceToTouchScreen = 0x0D0DFD40, + }, + Oversea = new() + { + MickeyWonderMethod = 138413872, + MickeyWonderMethodPartner = 4616352, + MickeyWonderMethodPartner2 = 108665520, + SetFieldOfView = 15428576, + SetEnableFogRendering = 276114192, + SetTargetFrameRate = 276640176, + OpenTeam = 101850864, + OpenTeamPageAccordingly = 127059184, + CheckCanEnter = 110655936, + SetupQuestBanner = 0x0DB113B0, + FindGameObject = 0x107C0CF0, + SetActive = 0x107C0A50, + EventCameraMove = 0x08BB0F30, + ShowOneDamageTextEx = 0x0AA05750, + SwitchInputDeviceToTouchScreen = 0x0CCD6800, + }, + }; + + Console.WriteLine(JsonSerializer.Serialize(wrapper, Options)); + } + + private sealed class UnlockerIslandConfigurationWrapper + { + public required UnlockerIslandFunctionOffset Chinese { get; set; } + + public required UnlockerIslandFunctionOffset Oversea { get; set; } + } + + private sealed class UnlockerIslandFunctionOffset + { + public required uint MickeyWonderMethod { get; set; } + + public required uint MickeyWonderMethodPartner { get; set; } + + public required uint MickeyWonderMethodPartner2 { get; set; } + + public required uint SetFieldOfView { get; set; } + + public required uint SetEnableFogRendering { get; set; } + + public required uint SetTargetFrameRate { get; set; } + + public required uint OpenTeam { get; set; } + + public required uint OpenTeamPageAccordingly { get; set; } + + public required uint CheckCanEnter { get; set; } + + public required uint SetupQuestBanner { get; set; } + + public required uint FindGameObject { get; set; } + + public required uint SetActive { get; set; } + + public required uint EventCameraMove { get; set; } + + public required uint ShowOneDamageTextEx { get; set; } + + public required uint SwitchInputDeviceToTouchScreen { get; set; } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/DependencyInjectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/DependencyInjectionTest.cs new file mode 100644 index 0000000..8cd0ebd --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/DependencyInjectionTest.cs @@ -0,0 +1,159 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; + +namespace Snap.Hutao.Test.PlatformExtensions; + +[TestClass] +public sealed class DependencyInjectionTest +{ + private readonly IServiceProvider services = new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddScoped() + .AddTransient() + .AddKeyedTransient("A") + .AddKeyedTransient("B") + .AddTransient(typeof(IGenericService<>), typeof(GenericService<>)) + .AddTransient(typeof(IGenericService), typeof(CloseGenericService)) + .AddLogging(builder => builder.AddConsole()) + .BuildServiceProvider(); + + [TestMethod] + public void OriginalTypeCannotResolved() + { + Assert.IsNull(services.GetService()); + Assert.IsNull(services.GetService()); + } + + [TestMethod] + public void GenericServicesCanBeResolved() + { + Assert.IsNotNull(services.GetService>()); + } + + [TestMethod] + public void CloseGenericSeriveCanBeResolved() + { + IGenericService service = services.GetRequiredService>(); + Assert.IsTrue(service is CloseGenericService); + } + + [TestMethod] + public void ScopedServiceInitializeOneTimeInScope() + { + using (IServiceScope scope = services.CreateScope()) + { + IScopedService service1 = scope.ServiceProvider.GetRequiredService(); + IScopedService service2 = scope.ServiceProvider.GetRequiredService(); + Assert.AreEqual(service1.Id, service2.Id); + } + } + + [TestMethod] + public void LoggerWithInterfaceTypeCanBeResolved() + { + Assert.IsNotNull(services.GetService>()); + Assert.IsNotNull(services.GetRequiredService().CreateLogger(nameof(IScopedService))); + } + + [TestMethod] + public void KeyedServicesCanNotBeResolvedAsEnumerable() + { + Assert.IsNotNull(services.GetRequiredKeyedService("A")); + Assert.IsNotNull(services.GetRequiredKeyedService("B")); + + Assert.AreEqual(1, services.GetServices().Count()); + } + + [TestMethod] + public void NoKeyServiceCanBeResolved() + { + Assert.IsNotNull(services.GetRequiredKeyedService("A")); + Assert.IsNotNull(services.GetRequiredService()); + } + + [TestMethod] + public void TransientServiceIsDisposedInScope() + { + IServiceProvider serviceProvider = new ServiceCollection().AddTransient().BuildServiceProvider(); + + DisposableService service; + + using (IServiceScope scope = serviceProvider.CreateScope()) + { + service = scope.ServiceProvider.GetRequiredService(); + } + + Assert.IsTrue(service.IsDisposed); + GC.KeepAlive(service); + } + + private interface IService + { + Guid Id { get; } + } + + private interface IScopedService + { + Guid Id { get; } + } + + private sealed class ServiceA : IService, IScopedService + { + public Guid Id { get; } = Guid.NewGuid(); + } + + private sealed class ServiceB : IService + { + public Guid Id + { + get => throw new NotImplementedException(); + } + } + + private interface IGenericService + { + } + + private sealed class GenericService : IGenericService + { + } + + private sealed class CloseGenericService : IGenericService + { + } + + private sealed class NonInjectedServiceA + { + } + + private sealed class NonInjectedServiceB + { + [ActivatorUtilitiesConstructor] + public NonInjectedServiceB(NonInjectedServiceA? serviceA) + { + } + } + + private interface IKeyedService; + + private sealed class NoKeyService : IKeyedService; + + private sealed class KeyedServiceA : IKeyedService; + + private sealed class KeyedServiceB : IKeyedService; + + private sealed class DisposableService : IDisposable + { + private bool isDisposed; + + public bool IsDisposed { get => isDisposed; } + + public void Dispose() + { + isDisposed = true; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/RateLimitingTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/RateLimitingTest.cs new file mode 100644 index 0000000..cc7a816 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/RateLimitingTest.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.RateLimiting; + +namespace Snap.Hutao.Test.PlatformExtensions; + +[TestClass] +public sealed class RateLimitingTest +{ + [TestMethod] + public void TokenBucketRateLimiterPrivateGetQueueCanAsObject() + { + TokenBucketRateLimiter rateLimiter = new(new() + { + ReplenishmentPeriod = TimeSpan.FromSeconds(1), + TokensPerPeriod = 1, + AutoReplenishment = true, + TokenLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + }); + + using (rateLimiter) + { + lock(PrivateGetLock(rateLimiter)) + { + Assert.IsNotNull(rateLimiter); + } + } + } + + // private object Lock => _queue + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Lock")] + private static extern object PrivateGetLock(TokenBucketRateLimiter rateLimiter); + + // private double _tokenCount; + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_tokenCount")] + private static extern ref double PrivateGetTokenCount(TokenBucketRateLimiter rateLimiter); + + // private readonly TokenBucketRateLimiterOptions _options; + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_options")] + private static extern ref readonly TokenBucketRateLimiterOptions PrivateGetOptions(TokenBucketRateLimiter rateLimiter); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/EnumRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/EnumRuntimeBehaviorTest.cs new file mode 100644 index 0000000..7cefd9c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/EnumRuntimeBehaviorTest.cs @@ -0,0 +1,37 @@ +using System; + +namespace Snap.Hutao.Test.RuntimeBehavior; + +[TestClass] +public sealed class EnumRuntimeBehaviorTest +{ + [TestMethod] + public void EnumParseCanNotHandleEmptyString() + { + Assert.Throws(() => + { + Enum.Parse(string.Empty); + }); + } + + [TestMethod] + public void EnumParseCanHandleNumberString() + { + EnumA a = Enum.Parse("2"); + Assert.AreEqual(a, EnumA.ValueB); + } + + [TestMethod] + public void EnumToStringDecimal() + { + Assert.AreEqual("2", EnumA.ValueB.ToString("D")); + } + + private enum EnumA + { + None = 0, + ValueA = 1, + ValueB = 2, + ValueC = 3, + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/ForEachRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/ForEachRuntimeBehaviorTest.cs new file mode 100644 index 0000000..2331f35 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/ForEachRuntimeBehaviorTest.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace Snap.Hutao.Test.RuntimeBehavior; + +[TestClass] +public sealed class ForEachRuntimeBehaviorTest +{ + [TestMethod] + public void ListOfStringCanEnumerateAsReadOnlySpanOfChar() + { + List strings = ["a", "b", "c"]; + int count = 0; + foreach (ReadOnlySpan chars in strings) + { + Assert.IsTrue(chars.Length == 1); + ++count; + } + + Assert.AreEqual(3, count); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/HttpClientBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/HttpClientBehaviorTest.cs new file mode 100644 index 0000000..5f7584f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/HttpClientBehaviorTest.cs @@ -0,0 +1,54 @@ +using System; +using System.Drawing; +using System.IO; +using System.Net.Http; +using System.Net.Http.Json; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Snap.Hutao.Test.RuntimeBehavior; + +[TestClass] +public sealed class HttpClientBehaviorTest +{ + private const int MessageNotYetSent = 0; + + [TestMethod] + public async Task RetrySendHttpRequestMessage() + { + foreach (Type nestType in typeof(HttpContent).GetNestedTypes(BindingFlags.NonPublic)) + { + Console.WriteLine(nestType.AssemblyQualifiedName); + } + + using (HttpClient httpClient = new()) + { + HttpRequestMessage requestMessage = new(HttpMethod.Post, "https://jsonplaceholder.typicode.com/posts"); + JsonContent content = JsonContent.Create(new Point(12, 34)); + requestMessage.Content = content; + using (requestMessage) + { + await httpClient.SendAsync(requestMessage).ConfigureAwait(false); + } + + Interlocked.Exchange(ref GetPrivateSendStatus(requestMessage), MessageNotYetSent); + Volatile.Write(ref GetPrivateDisposed(content), false); + await httpClient.SendAsync(requestMessage).ConfigureAwait(false); + } + } + + // private int _sendStatus + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_sendStatus")] + private static extern ref int GetPrivateSendStatus(HttpRequestMessage message); + + // private bool _disposed + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")] + private static extern ref bool GetPrivateDisposed(HttpRequestMessage message); + + // private bool _disposed + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")] + private static extern ref bool GetPrivateDisposed(HttpContent content); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/PropertyRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/PropertyRuntimeBehaviorTest.cs new file mode 100644 index 0000000..31c1091 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/PropertyRuntimeBehaviorTest.cs @@ -0,0 +1,15 @@ +using System; + +namespace Snap.Hutao.Test.RuntimeBehavior; + +[TestClass] +public sealed class PropertyRuntimeBehaviorTest +{ + [TestMethod] + public void GetTwiceOnPropertyResultsNotSame() + { + Assert.AreNotEqual(UUID, UUID); + } + + public static Guid UUID { get => Guid.NewGuid(); } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/RangeRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/RangeRuntimeBehaviorTest.cs new file mode 100644 index 0000000..57f7cd8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/RangeRuntimeBehaviorTest.cs @@ -0,0 +1,17 @@ +using System; + +namespace Snap.Hutao.Test.RuntimeBehavior; + +[TestClass] +public sealed class RangeRuntimeBehaviorTest +{ + [TestMethod] + public void RangeTrimLastOne() + { + int[] array = [1, 2, 3, 4]; + int[] test = [1, 2, 3]; + int[] result = array[..^1]; + Assert.AreEqual(3, result.Length); + Assert.IsTrue(MemoryExtensions.SequenceEqual(test, result)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/StringRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/StringRuntimeBehaviorTest.cs new file mode 100644 index 0000000..015a08b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/StringRuntimeBehaviorTest.cs @@ -0,0 +1,35 @@ +using System; + +namespace Snap.Hutao.Test.RuntimeBehavior; + +[TestClass] +public sealed class StringRuntimeBehaviorTest +{ + [TestMethod] + public unsafe void NullStringFixedIsNullPointer() + { + string testStr = null!; + fixed (char* pStr = testStr) + { + Assert.IsTrue(pStr == null); + } + } + + [TestMethod] + public unsafe void EmptyStringFixedIsNullTerminator() + { + string testStr = string.Empty; + fixed (char* pStr = testStr) + { + Assert.IsTrue(*pStr == '\0'); + } + } + + [TestMethod] + public unsafe void EmptyStringAsSpanIsZeroLength() + { + string testStr = string.Empty; + ReadOnlySpan testSpan = testStr; + Assert.IsTrue(testSpan.Length == 0); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs new file mode 100644 index 0000000..a84a234 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.Test.RuntimeBehavior; + +[TestClass] +public sealed class UnsafeRuntimeBehaviorTest +{ + [TestMethod] + public unsafe void UInt32AllSetIsUInt32MaxValue() + { + byte[] bytes = [0xFF, 0xFF, 0xFF, 0xFF]; + fixed (byte* pBytes = bytes) + { + Assert.AreEqual(uint.MaxValue, *(uint*)pBytes); + } + } + + [TestMethod] + public unsafe void UInt32LayoutIsLittleEndian() + { + ulong testValue = 0x1234567887654321; + ref BuildVersion version = ref Unsafe.As(ref testValue); + + Assert.AreEqual(0x1234, version.Major); + Assert.AreEqual(0x5678, version.Minor); + Assert.AreEqual(0x8765, version.Patch); + Assert.AreEqual(0x4321, version.Build); + } + + [TestMethod] + public unsafe void ReadOnlyStructCanBeModifiedInCtor() + { + TestStruct testStruct = new([4444, 7878, 5656, 1212]); + + Assert.AreEqual(4444, testStruct.Value1); + Assert.AreEqual(7878, testStruct.Value2); + Assert.AreEqual(5656, testStruct.Value3); + Assert.AreEqual(1212, testStruct.Value4); + } + + [TestMethod] + public unsafe void UnsafeLiteralUtf8StringReference() + { + void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference("test"u8)); + GC.Collect(GC.MaxGeneration); + ReadOnlySpan bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr); + Console.WriteLine(System.Text.Encoding.UTF8.GetString(bytes)); + } + + [TestMethod] + public unsafe void UnsafeSizeInt32ToRectInt32Test() + { + RectInt32 rectInt32 = ToRectInt32(new(100, 200)); + Assert.AreEqual(rectInt32.X, 0); + Assert.AreEqual(rectInt32.Y, 0); + Assert.AreEqual(rectInt32.Width, 100); + Assert.AreEqual(rectInt32.Height, 200); + + unsafe RectInt32 ToRectInt32(SizeInt32 sizeInt32) + { + byte* pBytes = stackalloc byte[sizeof(RectInt32)]; + *(SizeInt32*)(pBytes + 8) = sizeInt32; + return *(RectInt32*)pBytes; + } + } + + private struct RectInt32 + { + public int X; + public int Y; + public int Width; + public int Height; + + public RectInt32(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + } + + private struct SizeInt32 + { + public int Width; + public int Height; + + public SizeInt32(int width, int height) + { + Width = width; + Height = height; + } + } + + [SuppressMessage("", "CS0649")] + private readonly struct TestStruct + { + public readonly int Value1; + public readonly int Value2; + public readonly int Value3; + public readonly int Value4; + + public TestStruct(List list) + { + CollectionsMarshal.AsSpan(list).CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As(ref this), 4)); + } + } + + private readonly struct BuildVersion + { + public readonly ushort Build; + public readonly ushort Patch; + public readonly ushort Minor; + public readonly ushort Major; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj new file mode 100644 index 0000000..21ec36d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + preview + disable + enable + false + true + True + Debug;Release + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao.sln b/src/Snap.Hutao/Snap.Hutao.sln new file mode 100644 index 0000000..73b2408 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.sln @@ -0,0 +1,92 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11206.111 d18.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao", "Snap.Hutao\Snap.Hutao.csproj", "{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .vsconfig = .vsconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.Build.0 = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.Deploy.0 = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|ARM.ActiveCfg = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|ARM.Build.0 = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|ARM.Deploy.0 = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|arm64.ActiveCfg = Debug|arm64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|arm64.Build.0 = Debug|arm64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|arm64.Deploy.0 = Debug|arm64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|x64.ActiveCfg = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|x64.Build.0 = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|x64.Deploy.0 = Debug|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|x86.ActiveCfg = Debug|x86 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|x86.Build.0 = Debug|x86 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|x86.Deploy.0 = Debug|x86 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|Any CPU.ActiveCfg = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|Any CPU.Build.0 = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|Any CPU.Deploy.0 = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|ARM.ActiveCfg = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|ARM.Build.0 = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|ARM.Deploy.0 = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|arm64.ActiveCfg = Release|arm64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|arm64.Build.0 = Release|arm64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|arm64.Deploy.0 = Release|arm64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x64.ActiveCfg = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x64.Build.0 = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x64.Deploy.0 = Release|x64 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86 + {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86 + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|ARM.ActiveCfg = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|ARM.Build.0 = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.ActiveCfg = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.Build.0 = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x64.ActiveCfg = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x64.Build.0 = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x86.ActiveCfg = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x86.Build.0 = Debug|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|Any CPU.Build.0 = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|ARM.ActiveCfg = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|ARM.Build.0 = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|arm64.ActiveCfg = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|arm64.Build.0 = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.ActiveCfg = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU + {D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]} + RESX_ShowErrorsInErrorList = False + RESX_SortFileContentOnSave = True + SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA} + RESX_NeutralResourcesLanguage = zh-CN + RESX_AutoApplyExistingTranslations = False + EndGlobalSection +EndGlobal diff --git a/src/Snap.Hutao/Snap.Hutao.slnx b/src/Snap.Hutao/Snap.Hutao.slnx new file mode 100644 index 0000000..b81a126 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.slnx @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/.filenesting.json b/src/Snap.Hutao/Snap.Hutao/.filenesting.json new file mode 100644 index 0000000..f2af30c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/.filenesting.json @@ -0,0 +1,50 @@ +{ + "help": "https://go.microsoft.com/fwlink/?linkid=866610", + "dependentFileProviders": { + "add": { + "extensionToExtension": { + "add": { + ".json": [ + ".txt" + ] + } + }, + "pathSegment": { + "add": { + ".*": [ + ".cs", + ".resx", + ".appxmanifest" + ] + } + }, + "fileSuffixToExtension": { + "add": { + "DesignTimeFactory.cs": [".cs"] + } + }, + "fileToFile": { + "add": { + ".filenesting.json": [ + "App.xaml.cs" + ], + "app.manifest": [ + "App.xaml.cs" + ], + "Package.appxmanifest": [ + "App.xaml" + ], + "Package.StoreAssociation.xml": [ + "App.xaml" + ], + ".editorconfig": [ + "Bootstrap.cs" + ], + "GlobalUsing.cs": [ + "Bootstrap.cs" + ] + } + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ApiEndpoints.csv b/src/Snap.Hutao/Snap.Hutao/ApiEndpoints.csv new file mode 100644 index 0000000..a8645f7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ApiEndpoints.csv @@ -0,0 +1,85 @@ +Name,CN,OS +Extra:{ "Namespace": "Snap.Hutao.Web.Endpoint.Hoyolab" } +"string AccountCreateActionTicket()",https://passport-api.mihoyo.com/account/ma-cn-verifier/app/createActionTicketByToken, +"string AccountCreateAuthTicketByGameBiz()",https://passport-api.mihoyo.com/account/ma-cn-verifier/app/createAuthTicketByGameBiz,https://sg-public-api.hoyoverse.com/account/ma-verifier/api/createAuthTicketBySToken, +"string AccountCreateEmailCaptchaByActionTicket()",,https://sg-public-api.hoyoverse.com/account/ma-verifier/api/createEmailCaptchaByActionTicket, +"string AccountCreateLoginCaptcha()",https://passport-api.mihoyo.com/account/ma-cn-verifier/verifier/createLoginCaptcha, +"string AccountCreateQrLogin()",https://passport-api.mihoyo.com/account/ma-cn-passport/app/createQRLogin, +"string AccountGetActionTicketInfo()",,https://sg-public-api.hoyoverse.com/account/ma-verifier/api/getActionTicketInfo, +"string AccountGetCookieTokenBySToken()",https://passport-api.mihoyo.com/account/auth/api/getCookieAccountInfoBySToken,https://api-account-os.hoyoverse.com/account/auth/api/getCookieAccountInfoBySToken +"string AccountGetLTokenBySToken()",https://passport-api.mihoyo.com/account/auth/api/getLTokenBySToken,https://api-account-os.hoyoverse.com/account/auth/api/getLTokenBySToken +"string AccountGetSTokenByGameToken()",https://passport-api.mihoyo.com/account/ma-cn-session/app/getTokenByGameToken, +"string AccountGetSTokenByOldToken()",https://passport-api.mihoyo.com/account/ma-cn-session/app/getTokenBySToken, +"string AccountLoginByMobileCaptcha()",https://passport-api.mihoyo.com/account/ma-cn-passport/app/loginByMobileCaptcha, +"string AccountLoginByPassword()",https://passport-api.mihoyo.com/account/ma-cn-passport/app/loginByPassword,https://sg-public-api.hoyoverse.com/account/ma-passport/api/appLoginByPassword +"string AccountLoginByThirdParty()",,https://sg-public-api.hoyoverse.com/account/ma-passport/api/appLoginByThirdParty +"string AccountQueryQrLoginStatus()",https://passport-api.mihoyo.com/account/ma-cn-passport/app/queryQRLoginStatus, +"string AccountVerifyActionTicketPartly()",,https://sg-public-api.hoyoverse.com/account/ma-verifier/api/verifyActionTicketPartly, +"string AccountVerifyLtoken()",https://passport-api-v4.mihoyo.com/account/ma-cn-session/web/verifyLtoken, +"string ActHoyolabReferer()",,https://act.hoyolab.com/ +"string AnnContent(string languageCode, Snap.Hutao.Web.Hoyolab.Region region)","https://hk4e-ann-api.mihoyo.com/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery(languageCode, region)}","https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/announcement/api/getAnnContent?{AnnouncementQuery(languageCode, region)}" +"string AnnList(string languageCode, Snap.Hutao.Web.Hoyolab.Region region)","https://hk4e-ann-api.mihoyo.com/common/hk4e_cn/announcement/api/getAnnList?{AnnouncementQuery(languageCode, region)}","https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/announcement/api/getAnnList?{AnnouncementQuery(languageCode, region)}" +"string AnnouncementQuery(string languageCode, Snap.Hutao.Web.Hoyolab.Region region)",game=hk4e&game_biz=hk4e_cn&lang={languageCode}&bundle_id=hk4e_cn&platform=pc®ion={region}&level=55&uid=100000000,game=hk4e&game_biz=hk4e_global&lang={languageCode}&bundle_id=hk4e_global&platform=pc®ion={region}&level=55&uid=100000000 +"string AppAuthGenAuthKey()",https://app.mihoyo.com/account/auth/api/genAuthKey, +"string AppReferer()",https://app.mihoyo.com,https://app.hoyolab.com/ +"string AuthActionTicket(string actionType, string stoken, string uid)",https://api-takumi.mihoyo.com/auth/api/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid},https://api-account-os.hoyoverse.com/account/auth/api/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid} +"string AuthMultiToken(string loginTicket, string loginUid)",https://api-takumi.mihoyo.com/auth/api/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3,https://api-account-os.hoyoverse.com/account/auth/api/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3 +"string BbsReferer()",https://bbs.mihoyo.com/, +"string BindingGenAuthKey()",https://api-takumi.mihoyo.com/binding/api/genAuthKey,https://api-account-os.hoyoverse.com/binding/api/genAuthKey +"string CalculateAvatarList()",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/avatar/list,https://sg-public-api.hoyolab.com/event/e20200928calculate/v1/avatar/list +"string CalculateAvatarSkillList(Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar avatar)",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/avatarSkill/list?avatar_id={avatar.Id}&element_attr_id={(int)avatar.ElementAttrId}, +"string CalculateBatchCompute()",https://api-takumi.mihoyo.com/event/e20200928calculate/v3/batch_compute,https://sg-public-api.hoyolab.com/event/e20200928calculate/v3/batch_compute +"string CalculateCompute()",https://api-takumi.mihoyo.com/event/e20200928calculate/v2/compute,https://sg-public-api.hoyoverse.com/event/calculateos/compute +"string CalculateFurnitureBlueprint(string shareCode)",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/furniture/blueprint?share_code={shareCode},https://sg-public-api.hoyolab.com/event/e20200928calculate/v1/furniture/blueprint?share_code={shareCode} +"string CalculateFurnitureCompute()",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/furniture/compute,https://sg-public-api.hoyolab.com/event/e20200928calculate/v1/furniture/compute +"string CalculateFurnitureList()",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/furniture/list,https://sg-public-api.hoyolab.com/event/e20200928calculate/v1/furniture/list +"string CalculateSyncAvatarDetail(Model.Primitive.AvatarId avatarId, Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/sync/avatar/detail?avatar_id={avatarId.Value}&{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToUidRegionQueryString(uid)},https://sg-public-api.hoyoverse.com/event/calculateos/sync/avatar/detail?avatar_id={avatarId.Value}&{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToUidRegionQueryString(uid)} +"string CalculateSyncAvatarList()",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/sync/avatar/list,https://sg-public-api.hoyolab.com/event/e20200928calculate/v1/sync/avatar/list +"string CalculateWeaponList()",https://api-takumi.mihoyo.com/event/e20200928calculate/v1/weapon/list,https://sg-public-api.hoyolab.com/event/e20200928calculate/v1/weapon/list +"string CardCreateVerification(bool highRisk)","https://api-takumi-record.mihoyo.com/game_record/app/card/wapi/createVerification?is_high={(highRisk ? ""true"" : ""false"")}", +"string CardVerifyVerification()",https://api-takumi-record.mihoyo.com/game_record/app/card/wapi/verifyVerification, +"string CardWidgetData()",https://api-takumi-record.mihoyo.com/game_record/app/card/api/getWidgetData?game_id=2, +"string CardWidgetData2()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/aapi/widget/v2?game_id=2, +"string DeviceFpGetExtList(int platform)",https://public-data-api.mihoyo.com/device-fp/api/getExtList?platform={platform:D}, +"string DeviceFpGetFp()",https://public-data-api.mihoyo.com/device-fp/api/getFp, +"string GachaInfoGetGachaLog(string query)",https://public-operation-hk4e.mihoyo.com/gacha_info/api/getGachaLog?{query},https://public-operation-hk4e-sg.hoyoverse.com/gacha_info/api/getGachaLog?{query} +"string GameRecordCharacterList()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/character/list,https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/character/list +"string GameRecordCharacterDetail()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/character/detail,https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/character/detail +"string GameRecordDailyNote(Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/dailyNote?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)},https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/dailyNote?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)} +"string GameRecordDailyNotePath()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/dailyNote, +"string GameRecordHardChallenge(Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/hard_challenge?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)}&need_detail=true,https://sg-public-api.hoyolab.com/event/game_record/genshin/api/hard_challenge?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)}&need_detail=true +"string GameRecordHardChallengePath()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/hard_challenge, +"string GameRecordHardChallengePopularity(Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/hard_challenge/popularity?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)},https://sg-public-api.hoyolab.com/event/game_record/genshin/api/hard_challenge/popularity?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)} +"string GameRecordIndex(Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/index?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)},https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/index?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)} +"string GameRecordIndexPath()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/index, +"string GameRecordRoleBasicInfo(Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/roleBasicInfo?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)},https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/roleBasicInfo?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)} +"string GameRecordRoleCombat(Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/role_combat?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)}&need_detail=true&active=1,https://sg-public-api.hoyolab.com/event/game_record/genshin/api/role_combat?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)}&need_detail=true&active=1 +"string GameRecordRoleCombatPath()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/role_combat,https://sg-public-api.hoyolab.com/event/game_record/genshin/api/role_combat +"string GameRecordSpiralAbyss(Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.ScheduleType scheduleType, Snap.Hutao.Web.Hoyolab.PlayerUid uid)",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/spiralAbyss?schedule_type={(int)scheduleType}&{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)},https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/spiralAbyss?{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToRoleIdServerQueryString(uid)}&schedule_type={(int)scheduleType} +"string GameRecordSpiralAbyssPath()",https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/spiralAbyss, +"string HomeNew(int gid)",https://bbs-api.miyoushe.com/apihub/api/home/new?gids={gid}, +"string HoyoPlayConnectDeprecatedFileConfigs(Service.Game.Scheme.LaunchScheme scheme)",https://hyp-api.mihoyo.com/hyp/hyp-connect/api/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D},https://sg-hyp-api.hoyoverse.com/hyp/hyp-connect/api/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D} +"string HoyoPlayConnectGameBranches(Service.Game.Scheme.LaunchScheme scheme)",https://hyp-api.mihoyo.com/hyp/hyp-connect/api/getGameBranches?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId},https://sg-hyp-api.hoyoverse.com/hyp/hyp-connect/api/getGameBranches?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId} +"string HoyoPlayConnectGameChannelSDKs(Service.Game.Scheme.LaunchScheme scheme)",https://hyp-api.mihoyo.com/hyp/hyp-connect/api/getGameChannelSDKs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D},https://sg-hyp-api.hoyoverse.com/hyp/hyp-connect/api/getGameChannelSDKs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D} +"string HoyoPlayConnectGamePackages(Service.Game.Scheme.LaunchScheme scheme)",https://hyp-api.mihoyo.com/hyp/hyp-connect/api/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId},https://sg-hyp-api.hoyoverse.com/hyp/hyp-connect/api/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId} +"string LunaSolActivityId()",e202311201442471,e202102251931481 +"string LunaSolHome(string languageCode)",https://api-takumi.mihoyo.com/event/luna/home?lang={languageCode}&act_id={LunaSolActivityId()},https://sg-hk4e-api.hoyoverse.com/event/sol/home?lang={languageCode}&act_id={LunaSolActivityId()} +"string LunaSolInfo(Snap.Hutao.Web.Hoyolab.PlayerUid uid, string languageCode)",https://api-takumi.mihoyo.com/event/luna/info?lang={languageCode}&act_id={LunaSolActivityId()}&{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToUidRegionQueryString(uid)},https://sg-hk4e-api.hoyoverse.com/event/sol/info?lang={languageCode}&act_id={LunaSolActivityId()}&{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToUidRegionQueryString(uid)} +"string LunaSolReSign(string languageCode)",https://api-takumi.mihoyo.com/event/luna/resign,https://sg-hk4e-api.hoyoverse.com/event/sol/resign?lang={languageCode} +"string LunaSolResignInfo(Snap.Hutao.Web.Hoyolab.PlayerUid uid, string languageCode)",https://api-takumi.mihoyo.com/event/luna/resign_info?act_id={LunaSolActivityId()}&{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToUidRegionQueryString(uid)},https://sg-hk4e-api.hoyoverse.com/event/sol/resign_info?lang={languageCode}&act_id={LunaSolActivityId()}&{Snap.Hutao.Web.Hoyolab.PlayerUidExtension.ToUidRegionQueryString(uid)} +"string LunaSolSign()",https://api-takumi.mihoyo.com/event/luna/sign,https://sg-hk4e-api.hoyoverse.com/event/sol/sign?lang=zh-cn +"string MiyoliveRefreshCode()",https://api-takumi-static.mihoyo.com/event/miyolive/refreshCode?time={DateTimeOffset.Now.ToUnixTimeSeconds()}, +"string QrCodeFetch()",https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/fetch, +"string QrCodeQuery()",https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/query, +"string SophonChunkGetBuild(Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch.BranchWrapper branch)",https://downloader-api.mihoyo.com/downloader/sophon_chunk/api/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password}&tag={branch.Tag},https://sg-downloader-api.hoyoverse.com/downloader/sophon_chunk/api/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password}&tag={branch.Tag} +"string SophonChunkGetBuildNoTag(Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch.BranchWrapper branch)",https://downloader-api.mihoyo.com/downloader/sophon_chunk/api/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password},https://sg-downloader-api.hoyoverse.com/downloader/sophon_chunk/api/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password} +"string SophonChunkGetPatchBuild()",https://downloader-api.mihoyo.com/downloader/sophon_chunk/api/getPatchBuild,https://sg-downloader-api.hoyoverse.com/downloader/sophon_chunk/api/getPatchBuild +"string UserFullInfo()",https://bbs-api.mihoyo.com/user/wapi/getUserFullInfo?gids=2,https://bbs-api-os.hoyolab.com/community/user/wapi/getUserFullInfo?gid=2 +"string UserFullInfoQuery(string accountId)",https://bbs-api.mihoyo.com/user/wapi/getUserFullInfo?uid={accountId}&gids=2,https://bbs-api-os.hoyolab.com/community/painter/wapi/user/full +"string UserGameRolesByActionTicket(string actionTicket)",https://api-takumi.mihoyo.com/binding/api/getUserGameRoles?action_ticket={actionTicket}&game_biz=hk4e_cn, +"string UserGameRolesByCookie()",https://api-takumi.mihoyo.com/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn,https://api-os-takumi.hoyoverse.com/binding/api/getUserGameRolesByCookie?game_biz=hk4e_global +"string UserGameRolesByLtoken(Snap.Hutao.Web.Hoyolab.Region region)",,https://api-account-os.hoyoverse.com/binding/api/getUserGameRolesByLtoken?game_biz=hk4e_global®ion={region} +"string UserGameRolesBySToken()",https://api-takumi.mihoyo.com/binding/api/getUserGameRolesByStoken, +"string WebApiOsAccountLoginByCookie()",,https://webapi-os.account.hoyoverse.com/Api/login_by_cookie +"string WebLoginByPassword()",,https://api-account-os.hoyoverse.com/account/auth/api/webLoginByPassword +"string WebStaticReferer()",https://webstatic.mihoyo.com,https://webstatic-sea.mihoyo.com \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml b/src/Snap.Hutao/Snap.Hutao/App.xaml new file mode 100644 index 0000000..e76ae35 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs new file mode 100644 index 0000000..d05615d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -0,0 +1,131 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.Windows.AppLifecycle; +using Microsoft.Windows.AppNotifications; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Core.LifeCycle; +using Snap.Hutao.Core.LifeCycle.InterProcess; +using Snap.Hutao.Core.Logging; +using Snap.Hutao.Factory.Process; +using Snap.Hutao.Service; +using Snap.Hutao.UI.Xaml; +using Snap.Hutao.UI.Xaml.Control.Theme; +using System.Diagnostics; + +namespace Snap.Hutao; + +[Service(ServiceLifetime.Singleton)] +[SuppressMessage("", "SH001", Justification = "The App must be public")] +public sealed partial class App : Application +{ + private const string ConsoleBanner = """ + ---------------------------------------------------------------- + _____ _ _ _ + / ____| | | | | | | + | (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___ + \___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \ + ____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) | + |_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/ + | | + |_| + + Snap.Hutao is a open source software developed by DGP Studio. + Copyright (C) 2022 - 2025 DGP Studio, All Rights Reserved. + ---------------------------------------------------------------- + """; + + private readonly IServiceProvider serviceProvider; + private readonly IAppActivation activation; + private readonly ILogger logger; + + [GeneratedConstructor(InitializeComponent = true)] + public partial App(IServiceProvider serviceProvider); + + /// + /// Shortcut to get the instance. + /// + internal partial AppOptions Options { get; } + + partial void PostConstruct(IServiceProvider serviceProvider) + { + ExceptionHandling.Initialize(serviceProvider, this); + } + + [SuppressMessage("", "SA1202")] + public new void Exit() + { + XamlApplicationLifetime.Exiting = true; + SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting", "Hutao")); + SpinWait.SpinUntil(static () => XamlApplicationLifetime.ActivationAndInitializationCompleted); + base.Exit(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT(); + + try + { + // Important: You must call AppNotificationManager::Default().Register + // before calling AppInstance.GetCurrent.GetActivatedEventArgs. + AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked; + AppNotificationManager.Default.Register(); + + // E_INVALIDARG E_OUTOFMEMORY + AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + + if (serviceProvider.GetRequiredService().TryRedirectActivationTo(activatedEventArgs)) + { + SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao")); + XamlApplicationLifetime.ActivationAndInitializationCompleted = true; + Exit(); + return; + } + + logger.LogInformation($"{ConsoleBanner}"); + + FrameworkTheming.SetTheme(ThemeHelper.ElementToFramework(serviceProvider.GetRequiredService().ElementTheme.Value)); + + // Manually invoke + SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application")); + activation.ActivateAndInitialize(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)); + } + catch (Exception ex) + { + SentrySdk.CaptureException(ex); + SentrySdk.Flush(); + + ProcessFactory.KillCurrent(); + } + } + + [Conditional("DEBUG")] + private static void DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT() + { + // Extremely dangerous patch to workaround XamlDiagnostics::RemoveRootObjectFromLVT crashing when + // Window is closed during debugging. at LiveVisualTree.cpp line 423 + // -> if (m_visualTreeCallback && SUCCEEDED(m_visualTreeCallback.As(&xamlRootCallback))) + // We simply fail this check to skip the rest if block. + // As a result, Visual Studio Live Visual Tree can leave a DesktopWindowXamlSource without child. + // But the RuntimeObject is actually closed properly. + + // If no debugger is attached, do not patch. There will be no diagnostics LVT. + if (Debugger.IsAttached) + { + // 74 65 jz short loc_8E219D + // 48 8D 55 F0 lea root, [rbp+50h + p] ; p + // 48 8B CB mov this, rbx; this + // E8 58 DF FF FF call ??$As @UIVisualTreeServiceCallback3@@@?$ComPtr @UIVisualTreeServiceCallback@@@WRL @Microsoft@@QEBAJV ?$ComPtrRef @V?$ComPtr @UIVisualTreeServiceCallback3@@@WRL @Microsoft@@@Details@12@@Z; Microsoft::WRL::ComPtr < IVisualTreeServiceCallback >::As(Microsoft::WRL::Details::ComPtrRef>) + // 85 C0 test eax, eax + // 78 55 js short loc_8E219D + // Should be 78 xx (js near) + Win32.MemoryUtilities.Patch("Microsoft.ui.xaml.dll", 0x008E2146, 2, static codes => + { + // Rewrite to jmp + codes[0] = 0xEB; + }); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.ps1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.ps1 new file mode 100644 index 0000000..f82b0b3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.ps1 @@ -0,0 +1,866 @@ +# +# Add-AppxDevPackage.ps1 is a PowerShell script designed to install app +# packages created by Visual Studio for developers. To run this script from +# Explorer, right-click on its icon and choose "Run with PowerShell". +# +# Visual Studio supplies this script in the folder generated with its +# "Prepare Package" command. The same folder will also contain the app +# package (a .appx file), the signing certificate (a .cer file), and a +# "Dependencies" subfolder containing all the framework packages used by the +# app. +# +# This script simplifies installing these packages by automating the +# following functions: +# 1. Find the app package and signing certificate in the script directory +# 2. Prompt the user to acquire a developer license and to install the +# certificate if necessary +# 3. Find dependency packages that are applicable to the operating system's +# CPU architecture +# 4. Install the package along with all applicable dependencies +# +# All command line parameters are reserved for use internally by the script. +# Users should launch this script from Explorer. +# + +# .Link +# http://go.microsoft.com/fwlink/?LinkId=243053 + +param( + [switch]$Force = $false, + [switch]$GetDeveloperLicense = $false, + [switch]$SkipLoggingTelemetry = $false, + [string]$CertificatePath = $null +) + +$ErrorActionPreference = "Stop" + +# The language resources for this script are placed in the +# "Add-AppDevPackage.resources" subfolder alongside the script. Since the +# current working directory might not be the directory that contains the +# script, we need to create the full path of the resources directory to +# pass into Import-LocalizedData +$ScriptPath = $null +try +{ + $ScriptPath = (Get-Variable MyInvocation).Value.MyCommand.Path + $ScriptDir = Split-Path -Parent $ScriptPath +} +catch {} + +if (!$ScriptPath) +{ + PrintMessageAndExit $UiStrings.ErrorNoScriptPath $ErrorCodes.NoScriptPath +} + +$LocalizedResourcePath = Join-Path $ScriptDir "Add-AppDevPackage.resources" +Import-LocalizedData -BindingVariable UiStrings -BaseDirectory $LocalizedResourcePath + +$ErrorCodes = Data { + ConvertFrom-StringData @' + Success = 0 + NoScriptPath = 1 + NoPackageFound = 2 + ManyPackagesFound = 3 + NoCertificateFound = 4 + ManyCertificatesFound = 5 + BadCertificate = 6 + PackageUnsigned = 7 + CertificateMismatch = 8 + ForceElevate = 9 + LaunchAdminFailed = 10 + GetDeveloperLicenseFailed = 11 + InstallCertificateFailed = 12 + AddPackageFailed = 13 + ForceDeveloperLicense = 14 + CertUtilInstallFailed = 17 + CertIsCA = 18 + BannedEKU = 19 + NoBasicConstraints = 20 + NoCodeSigningEku = 21 + InstallCertificateCancelled = 22 + BannedKeyUsage = 23 + ExpiredCertificate = 24 +'@ +} + +$IMAGE_FILE_MACHINE_i386 = 0x014c +$IMAGE_FILE_MACHINE_AMD64 = 0x8664 +$IMAGE_FILE_MACHINE_ARM64 = 0xAA64 +$IMAGE_FILE_MACHINE_ARM = 0x01c0 +$IMAGE_FILE_MACHINE_THUMB = 0x01c2 +$IMAGE_FILE_MACHINE_ARMNT = 0x01c4 + +# First try to use IsWow64Process2 to determine the OS architecture +try +{ + $IsWow64Process2_MethodDefinition = @" +[DllImport("kernel32.dll", SetLastError = true)] +public static extern bool IsWow64Process2(IntPtr process, out ushort processMachine, out ushort nativeMachine); +"@ + + $Kernel32 = Add-Type -MemberDefinition $IsWow64Process2_MethodDefinition -Name "Kernel32" -Namespace "Win32" -PassThru + + $Proc = Get-Process -id $pid + $processMachine = New-Object uint16 + $nativeMachine = New-Object uint16 + + $Res = $Kernel32::IsWow64Process2($Proc.Handle, [ref] $processMachine, [ref] $nativeMachine) + if ($Res -eq $True) + { + switch ($nativeMachine) + { + $IMAGE_FILE_MACHINE_i386 { $ProcessorArchitecture = "x86" } + $IMAGE_FILE_MACHINE_AMD64 { $ProcessorArchitecture = "amd64" } + $IMAGE_FILE_MACHINE_ARM64 { $ProcessorArchitecture = "arm64" } + $IMAGE_FILE_MACHINE_ARM { $ProcessorArchitecture = "arm" } + $IMAGE_FILE_MACHINE_THUMB { $ProcessorArchitecture = "arm" } + $IMAGE_FILE_MACHINE_ARMNT { $ProcessorArchitecture = "arm" } + } + } +} +catch +{ + # Ignore exception and fall back to using environment variables to determine the OS architecture +} + +if ($null -eq $ProcessorArchitecture) +{ + $ProcessorArchitecture = $Env:Processor_Architecture + + # Getting $Env:Processor_Architecture on arm64 machines will return x86. So check if the environment + # variable "ProgramFiles(Arm)" is also set, if it is we know the actual processor architecture is arm64. + # The value will also be x86 on amd64 machines when running the x86 version of PowerShell. + if ($ProcessorArchitecture -eq "x86") + { + if ($null -ne ${Env:ProgramFiles(Arm)}) + { + $ProcessorArchitecture = "arm64" + } + elseif ($null -ne ${Env:ProgramFiles(x86)}) + { + $ProcessorArchitecture = "amd64" + } + } +} + +function PrintMessageAndExit($ErrorMessage, $ReturnCode) +{ + Write-Host $ErrorMessage + try + { + # Log telemetry data regarding the use of the script if possible. + # There are three ways that this can be disabled: + # 1. If the "TelemetryDependencies" folder isn't present. This can be excluded at build time by setting the MSBuild property AppxLogTelemetryFromSideloadingScript to false + # 2. If the SkipLoggingTelemetry switch is passed to this script. + # 3. If Visual Studio telemetry is disabled via the registry. + $TelemetryAssembliesFolder = (Join-Path $PSScriptRoot "TelemetryDependencies") + if (!$SkipLoggingTelemetry -And (Test-Path $TelemetryAssembliesFolder)) + { + $job = Start-Job -FilePath (Join-Path $TelemetryAssembliesFolder "LogSideloadingTelemetry.ps1") -ArgumentList $TelemetryAssembliesFolder, "VS/DesignTools/SideLoadingScript/AddAppDevPackage", $ReturnCode, $ProcessorArchitecture + Wait-Job -Job $job -Timeout 60 | Out-Null + } + } + catch + { + # Ignore telemetry errors + } + + if (!$Force) + { + Pause + } + + exit $ReturnCode +} + +# +# Warns the user about installing certificates, and presents a Yes/No prompt +# to confirm the action. The default is set to No. +# +function ConfirmCertificateInstall +{ + $Answer = $host.UI.PromptForChoice( + "", + $UiStrings.WarningInstallCert, + [System.Management.Automation.Host.ChoiceDescription[]]@($UiStrings.PromptYesString, $UiStrings.PromptNoString), + 1) + + return $Answer -eq 0 +} + +# +# Validates whether a file is a valid certificate using CertUtil. +# This needs to be done before calling Get-PfxCertificate on the file, otherwise +# the user will get a cryptic "Password: " prompt for invalid certs. +# +function ValidateCertificateFormat($FilePath) +{ + # certutil -verify prints a lot of text that we don't need, so it's redirected to $null here + certutil.exe -verify $FilePath > $null + if ($LastExitCode -lt 0) + { + PrintMessageAndExit ($UiStrings.ErrorBadCertificate -f $FilePath, $LastExitCode) $ErrorCodes.BadCertificate + } + + # Check if certificate is expired + $cert = Get-PfxCertificate $FilePath + if (($cert.NotBefore -gt (Get-Date)) -or ($cert.NotAfter -lt (Get-Date))) + { + PrintMessageAndExit ($UiStrings.ErrorExpiredCertificate -f $FilePath) $ErrorCodes.ExpiredCertificate + } +} + +# +# Verify that the developer certificate meets the following restrictions: +# - The certificate must contain a Basic Constraints extension, and its +# Certificate Authority (CA) property must be false. +# - The certificate's Key Usage extension must be either absent, or set to +# only DigitalSignature. +# - The certificate must contain an Extended Key Usage (EKU) extension with +# Code Signing usage. +# - The certificate must NOT contain any other EKU except Code Signing and +# Lifetime Signing. +# +# These restrictions are enforced to decrease security risks that arise from +# trusting digital certificates. +# +function CheckCertificateRestrictions +{ + Set-Variable -Name BasicConstraintsExtensionOid -Value "2.5.29.19" -Option Constant + Set-Variable -Name KeyUsageExtensionOid -Value "2.5.29.15" -Option Constant + Set-Variable -Name EkuExtensionOid -Value "2.5.29.37" -Option Constant + Set-Variable -Name CodeSigningEkuOid -Value "1.3.6.1.5.5.7.3.3" -Option Constant + Set-Variable -Name LifetimeSigningEkuOid -Value "1.3.6.1.4.1.311.10.3.13" -Option Constant + Set-Variable -Name UwpSigningEkuOid -Value "1.3.6.1.4.1.311.84.3.1" -Option Constant + Set-Variable -Name DisposableSigningEkuOid -Value "1.3.6.1.4.1.311.84.3.2" -Option Constant + + $CertificateExtensions = (Get-PfxCertificate $CertificatePath).Extensions + $HasBasicConstraints = $false + $HasCodeSigningEku = $false + + foreach ($Extension in $CertificateExtensions) + { + # Certificate must contain the Basic Constraints extension + if ($Extension.oid.value -eq $BasicConstraintsExtensionOid) + { + # CA property must be false + if ($Extension.CertificateAuthority) + { + PrintMessageAndExit $UiStrings.ErrorCertIsCA $ErrorCodes.CertIsCA + } + $HasBasicConstraints = $true + } + + # If key usage is present, it must be set to digital signature + elseif ($Extension.oid.value -eq $KeyUsageExtensionOid) + { + if ($Extension.KeyUsages -ne "DigitalSignature") + { + PrintMessageAndExit ($UiStrings.ErrorBannedKeyUsage -f $Extension.KeyUsages) $ErrorCodes.BannedKeyUsage + } + } + + elseif ($Extension.oid.value -eq $EkuExtensionOid) + { + # Certificate must contain the Code Signing EKU + $EKUs = $Extension.EnhancedKeyUsages.Value + if ($EKUs -contains $CodeSigningEkuOid) + { + $HasCodeSigningEKU = $True + } + + # EKUs other than code signing and lifetime signing are not allowed + foreach ($EKU in $EKUs) + { + if ($EKU -ne $CodeSigningEkuOid -and $EKU -ne $LifetimeSigningEkuOid -and $EKU -ne $UwpSigningEkuOid -and $EKU -ne $DisposableSigningEkuOid) + { + PrintMessageAndExit ($UiStrings.ErrorBannedEKU -f $EKU) $ErrorCodes.BannedEKU + } + } + } + } + + if (!$HasBasicConstraints) + { + PrintMessageAndExit $UiStrings.ErrorNoBasicConstraints $ErrorCodes.NoBasicConstraints + } + if (!$HasCodeSigningEKU) + { + PrintMessageAndExit $UiStrings.ErrorNoCodeSigningEku $ErrorCodes.NoCodeSigningEku + } +} + +# +# Performs operations that require administrative privileges: +# - Prompt the user to obtain a developer license +# - Install the developer certificate (if -Force is not specified, also prompts the user to confirm) +# +function DoElevatedOperations +{ + if ($GetDeveloperLicense) + { + Write-Host $UiStrings.GettingDeveloperLicense + + if ($Force) + { + PrintMessageAndExit $UiStrings.ErrorForceDeveloperLicense $ErrorCodes.ForceDeveloperLicense + } + try + { + Show-WindowsDeveloperLicenseRegistration + } + catch + { + $Error[0] # Dump details about the last error + PrintMessageAndExit $UiStrings.ErrorGetDeveloperLicenseFailed $ErrorCodes.GetDeveloperLicenseFailed + } + } + + if ($CertificatePath) + { + Write-Host $UiStrings.InstallingCertificate + + # Make sure certificate format is valid and usage constraints are followed + ValidateCertificateFormat $CertificatePath + CheckCertificateRestrictions + + # If -Force is not specified, warn the user and get consent + if ($Force -or (ConfirmCertificateInstall)) + { + # Add cert to store + certutil.exe -addstore TrustedPeople $CertificatePath + if ($LastExitCode -lt 0) + { + PrintMessageAndExit ($UiStrings.ErrorCertUtilInstallFailed -f $LastExitCode) $ErrorCodes.CertUtilInstallFailed + } + } + else + { + PrintMessageAndExit $UiStrings.ErrorInstallCertificateCancelled $ErrorCodes.InstallCertificateCancelled + } + } +} + +# +# Checks whether the machine is missing a valid developer license. +# +function CheckIfNeedDeveloperLicense +{ + $Result = $true + try + { + $Result = (Get-WindowsDeveloperLicense | Where-Object { $_.IsValid } | Measure-Object).Count -eq 0 + } + catch {} + + return $Result +} + +# +# Launches an elevated process running the current script to perform tasks +# that require administrative privileges. This function waits until the +# elevated process terminates, and checks whether those tasks were successful. +# +function LaunchElevated +{ + # Set up command line arguments to the elevated process + $RelaunchArgs = '-ExecutionPolicy Unrestricted -file "' + $ScriptPath + '"' + + if ($Force) + { + $RelaunchArgs += ' -Force' + } + if ($NeedDeveloperLicense) + { + $RelaunchArgs += ' -GetDeveloperLicense' + } + if ($SkipLoggingTelemetry) + { + $RelaunchArgs += ' -SkipLoggingTelemetry' + } + if ($NeedInstallCertificate) + { + $RelaunchArgs += ' -CertificatePath "' + $DeveloperCertificatePath.FullName + '"' + } + + # Launch the process and wait for it to finish + try + { + $PowerShellExePath = (Get-Process -Id $PID).Path + $AdminProcess = Start-Process $PowerShellExePath -Verb RunAs -ArgumentList $RelaunchArgs -PassThru + } + catch + { + $Error[0] # Dump details about the last error + PrintMessageAndExit $UiStrings.ErrorLaunchAdminFailed $ErrorCodes.LaunchAdminFailed + } + + while (!($AdminProcess.HasExited)) + { + Start-Sleep -Seconds 2 + } + + # Check if all elevated operations were successful + if ($NeedDeveloperLicense) + { + if (CheckIfNeedDeveloperLicense) + { + PrintMessageAndExit $UiStrings.ErrorGetDeveloperLicenseFailed $ErrorCodes.GetDeveloperLicenseFailed + } + else + { + Write-Host $UiStrings.AcquireLicenseSuccessful + } + } + if ($NeedInstallCertificate) + { + $Signature = Get-AuthenticodeSignature $DeveloperPackagePath -Verbose + if ($Signature.Status -ne "Valid") + { + PrintMessageAndExit ($UiStrings.ErrorInstallCertificateFailed -f $Signature.Status) $ErrorCodes.InstallCertificateFailed + } + else + { + Write-Host $UiStrings.InstallCertificateSuccessful + } + } +} + +# +# Finds all applicable dependency packages according to OS architecture, and +# installs the developer package with its dependencies. The expected layout +# of dependencies is: +# +# +# \Dependencies +# .appx\.msix +# \x86 +# .appx\.msix +# \x64 +# .appx\.msix +# \arm +# .appx\.msix +# \arm64 +# .appx\.msix +# +function InstallPackageWithDependencies +{ + $DependencyPackagesDir = (Join-Path $ScriptDir "Dependencies") + $DependencyPackages = @() + if (Test-Path $DependencyPackagesDir) + { + # Get architecture-neutral dependencies + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "*.appx") | Where-Object { $_.Mode -NotMatch "d" } + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "*.msix") | Where-Object { $_.Mode -NotMatch "d" } + + # Get architecture-specific dependencies + if (($ProcessorArchitecture -eq "x86" -or $ProcessorArchitecture -eq "amd64" -or $ProcessorArchitecture -eq "arm64") -and (Test-Path (Join-Path $DependencyPackagesDir "x86"))) + { + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "x86\*.appx") | Where-Object { $_.Mode -NotMatch "d" } + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "x86\*.msix") | Where-Object { $_.Mode -NotMatch "d" } + } + if (($ProcessorArchitecture -eq "amd64") -and (Test-Path (Join-Path $DependencyPackagesDir "x64"))) + { + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "x64\*.appx") | Where-Object { $_.Mode -NotMatch "d" } + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "x64\*.msix") | Where-Object { $_.Mode -NotMatch "d" } + } + if (($ProcessorArchitecture -eq "arm" -or $ProcessorArchitecture -eq "arm64") -and (Test-Path (Join-Path $DependencyPackagesDir "arm"))) + { + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "arm\*.appx") | Where-Object { $_.Mode -NotMatch "d" } + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "arm\*.msix") | Where-Object { $_.Mode -NotMatch "d" } + } + if (($ProcessorArchitecture -eq "arm64") -and (Test-Path (Join-Path $DependencyPackagesDir "arm64"))) + { + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "arm64\*.appx") | Where-Object { $_.Mode -NotMatch "d" } + $DependencyPackages += Get-ChildItem (Join-Path $DependencyPackagesDir "arm64\*.msix") | Where-Object { $_.Mode -NotMatch "d" } + } + } + Write-Host $UiStrings.InstallingPackage + + $AddPackageSucceeded = $False + try + { + if ($DependencyPackages.FullName.Count -gt 0) + { + Write-Host $UiStrings.DependenciesFound + $DependencyPackages.FullName + Add-AppxPackage -Path $DeveloperPackagePath.FullName -DependencyPath $DependencyPackages.FullName -ForceApplicationShutdown + } + else + { + Add-AppxPackage -Path $DeveloperPackagePath.FullName -ForceApplicationShutdown + } + $AddPackageSucceeded = $? + } + catch + { + $Error[0] # Dump details about the last error + } + + if (!$AddPackageSucceeded) + { + if ($NeedInstallCertificate) + { + PrintMessageAndExit $UiStrings.ErrorAddPackageFailedWithCert $ErrorCodes.AddPackageFailed + } + else + { + PrintMessageAndExit $UiStrings.ErrorAddPackageFailed $ErrorCodes.AddPackageFailed + } + } +} + +# +# Main script logic when the user launches the script without parameters. +# +function DoStandardOperations +{ + # Check for an .appx or .msix file in the script directory + $PackagePath = Get-ChildItem (Join-Path $ScriptDir "*.appx") | Where-Object { $_.Mode -NotMatch "d" } + if ($PackagePath -eq $null) + { + $PackagePath = Get-ChildItem (Join-Path $ScriptDir "*.msix") | Where-Object { $_.Mode -NotMatch "d" } + } + $PackageCount = ($PackagePath | Measure-Object).Count + + # Check for an .appxbundle or .msixbundle file in the script directory + $BundlePath = Get-ChildItem (Join-Path $ScriptDir "*.appxbundle") | Where-Object { $_.Mode -NotMatch "d" } + if ($BundlePath -eq $null) + { + $BundlePath = Get-ChildItem (Join-Path $ScriptDir "*.msixbundle") | Where-Object { $_.Mode -NotMatch "d" } + } + $BundleCount = ($BundlePath | Measure-Object).Count + + # Check for an .eappx or .emsix file in the script directory + $EncryptedPackagePath = Get-ChildItem (Join-Path $ScriptDir "*.eappx") | Where-Object { $_.Mode -NotMatch "d" } + if ($EncryptedPackagePath -eq $null) + { + $EncryptedPackagePath = Get-ChildItem (Join-Path $ScriptDir "*.emsix") | Where-Object { $_.Mode -NotMatch "d" } + } + $EncryptedPackageCount = ($EncryptedPackagePath | Measure-Object).Count + + # Check for an .eappxbundle or .emsixbundle file in the script directory + $EncryptedBundlePath = Get-ChildItem (Join-Path $ScriptDir "*.eappxbundle") | Where-Object { $_.Mode -NotMatch "d" } + if ($EncryptedBundlePath -eq $null) + { + $EncryptedBundlePath = Get-ChildItem (Join-Path $ScriptDir "*.emsixbundle") | Where-Object { $_.Mode -NotMatch "d" } + } + $EncryptedBundleCount = ($EncryptedBundlePath | Measure-Object).Count + + $NumberOfPackages = $PackageCount + $EncryptedPackageCount + $NumberOfBundles = $BundleCount + $EncryptedBundleCount + + # There must be at least one package or bundle + if ($NumberOfPackages + $NumberOfBundles -lt 1) + { + PrintMessageAndExit $UiStrings.ErrorNoPackageFound $ErrorCodes.NoPackageFound + } + # We must have exactly one bundle OR no bundle and exactly one package + elseif ($NumberOfBundles -gt 1 -or + ($NumberOfBundles -eq 0 -and $NumberOfpackages -gt 1)) + { + PrintMessageAndExit $UiStrings.ErrorManyPackagesFound $ErrorCodes.ManyPackagesFound + } + + # First attempt to install a bundle or encrypted bundle. If neither exists, fall back to packages and then encrypted packages + if ($BundleCount -eq 1) + { + $DeveloperPackagePath = $BundlePath + Write-Host ($UiStrings.BundleFound -f $DeveloperPackagePath.FullName) + } + elseif ($EncryptedBundleCount -eq 1) + { + $DeveloperPackagePath = $EncryptedBundlePath + Write-Host ($UiStrings.EncryptedBundleFound -f $DeveloperPackagePath.FullName) + } + elseif ($PackageCount -eq 1) + { + $DeveloperPackagePath = $PackagePath + Write-Host ($UiStrings.PackageFound -f $DeveloperPackagePath.FullName) + } + elseif ($EncryptedPackageCount -eq 1) + { + $DeveloperPackagePath = $EncryptedPackagePath + Write-Host ($UiStrings.EncryptedPackageFound -f $DeveloperPackagePath.FullName) + } + + # The package must be signed + $PackageSignature = Get-AuthenticodeSignature $DeveloperPackagePath + $PackageCertificate = $PackageSignature.SignerCertificate + if (!$PackageCertificate) + { + PrintMessageAndExit $UiStrings.ErrorPackageUnsigned $ErrorCodes.PackageUnsigned + } + + # Test if the package signature is trusted. If not, the corresponding certificate + # needs to be present in the current directory and needs to be installed. + $NeedInstallCertificate = ($PackageSignature.Status -ne "Valid") + + if ($NeedInstallCertificate) + { + # List all .cer files in the script directory + $DeveloperCertificatePath = Get-ChildItem (Join-Path $ScriptDir "*.cer") | Where-Object { $_.Mode -NotMatch "d" } + $DeveloperCertificateCount = ($DeveloperCertificatePath | Measure-Object).Count + + # There must be exactly 1 certificate + if ($DeveloperCertificateCount -lt 1) + { + PrintMessageAndExit $UiStrings.ErrorNoCertificateFound $ErrorCodes.NoCertificateFound + } + elseif ($DeveloperCertificateCount -gt 1) + { + PrintMessageAndExit $UiStrings.ErrorManyCertificatesFound $ErrorCodes.ManyCertificatesFound + } + + Write-Host ($UiStrings.CertificateFound -f $DeveloperCertificatePath.FullName) + + # The .cer file must have the format of a valid certificate + ValidateCertificateFormat $DeveloperCertificatePath + + # The package signature must match the certificate file + if ($PackageCertificate -ne (Get-PfxCertificate $DeveloperCertificatePath)) + { + PrintMessageAndExit $UiStrings.ErrorCertificateMismatch $ErrorCodes.CertificateMismatch + } + } + + $NeedDeveloperLicense = CheckIfNeedDeveloperLicense + + # Relaunch the script elevated with the necessary parameters if needed + if ($NeedDeveloperLicense -or $NeedInstallCertificate) + { + Write-Host $UiStrings.ElevateActions + if ($NeedDeveloperLicense) + { + Write-Host $UiStrings.ElevateActionDevLicense + } + if ($NeedInstallCertificate) + { + Write-Host $UiStrings.ElevateActionCertificate + } + + $IsAlreadyElevated = ([Security.Principal.WindowsIdentity]::GetCurrent().Groups.Value -contains "S-1-5-32-544") + if ($IsAlreadyElevated) + { + if ($Force -and $NeedDeveloperLicense) + { + PrintMessageAndExit $UiStrings.ErrorForceDeveloperLicense $ErrorCodes.ForceDeveloperLicense + } + if ($Force -and $NeedInstallCertificate) + { + Write-Warning $UiStrings.WarningInstallCert + } + } + else + { + if ($Force) + { + PrintMessageAndExit $UiStrings.ErrorForceElevate $ErrorCodes.ForceElevate + } + else + { + Write-Host $UiStrings.ElevateActionsContinue + Pause + } + } + + LaunchElevated + } + + InstallPackageWithDependencies +} + +# +# Main script entry point +# +if ($GetDeveloperLicense -or $CertificatePath) +{ + DoElevatedOperations +} +else +{ + DoStandardOperations + PrintMessageAndExit $UiStrings.Success $ErrorCodes.Success +} + +# SIG # Begin signature block +# MIIhggYJKoZIhvcNAQcCoIIhczCCIW8CAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAaovVnr86uRLc7 +# aU4MWMaEBdFBUDgUiMNw07NprqcvsqCCC3IwggT6MIID4qADAgECAhMzAAADJUiy +# nQ5/xfQfAAAAAAMlMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTAwHhcNMjAwMzA0MTgyOTI5WhcNMjEwMzAzMTgyOTI5WjB0MQsw +# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u +# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +# AQCjpRI2NHmdF4E+oz+32gQNFWfiWA/gW26xpPqf0l47t99p7IIKd5CuTAMePNYW +# XHST8pFfb8yaTNWz6nECabhQTCIxAqtAzVpCNWXiuQDe18eEUoUFN2sgoMhpU7gb +# 0gZigbhvznmT0moq7orBEAMcrW6C88+9JyqWBgDK0MBbpxjIwBv0uPgj3R40ItML +# Qw9Lb0SBnriOEPQKGDCO2AI6MSi++xe5YXOkQZrLCDc6Tl/f/fTzn1Ci+JR7YJMd +# dq8f2Ne42ogsUVIW6JH8SKbLQXb9xOVn4fMiG9b6PgRugApS0IKAUI8OQQ2kSr2a +# 1BsKEY9B7MNUeFBXB74OrutZAgMBAAGjggF5MIIBdTAfBgNVHSUEGDAWBgorBgEE +# AYI3PQYBBggrBgEFBQcDAzAdBgNVHQ4EFgQULcKPAJ0r4hUrTVSYmpa5RA+uHnww +# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 +# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzA4NjUrNDU4NDkzMB8GA1UdIwQYMBaAFOb8 +# X3u7IgBY5HJOtfQhdCMy5u+sMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu +# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY0NvZFNpZ1BDQV8yMDEw +# LTA3LTA2LmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93 +# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljQ29kU2lnUENBXzIwMTAtMDct +# MDYuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBAFxz4O+cWeBo +# 86e5EImiUeJXoJ5huJwH6l3YUBLhBt+t+uE6zDtBqmygeAq+qMs3otaucTmO6VEy +# LRACa7Yx8xxDLK7MAcnxwAY6SYjciErNsDf1tApeZkCIINFW/8S2QKMSQXf4OJol +# jWHo1TkniL9IRmzviN9l42NYNJB9i71ezxP+6ZN4PDWi8QVe70dGCLl9O2RxPQFh +# Ecl3jWdCu5C1FDRg6qMpcx3qseQR2QF4+d4EE/UQ1h3YeShbtuzxf0ksbBnQqVU2 +# ZJ9E/GJUTWUSsYxsJnG8xg3G46Jz3ttfVE3coMLKh1fHqsI3XXIlVzT3BIx3N9nL +# g18hwONtu5kwggZwMIIEWKADAgECAgphDFJMAAAAAAADMA0GCSqGSIb3DQEBCwUA +# MIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH +# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQD +# EylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0x +# MDA3MDYyMDQwMTdaFw0yNTA3MDYyMDUwMTdaMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpDmRQ +# eWe1xOP9CQBMnpSs91Zo6kTYz8VYT6mldnxtRbrTOZK0pB75+WWC5BfSj/1EnAjo +# ZZPOLFWEv30I4y4rqEErGLeiS25JTGsVB97R0sKJHnGUzbV/S7SvCNjMiNZrF5Q6 +# k84mP+zm/jSYV9UdXUn2siou1YW7WT/4kLQrg3TKK7M7RuPwRknBF2ZUyRy9HcRV +# Yldy+Ge5JSA03l2mpZVeqyiAzdWynuUDtWPTshTIwciKJgpZfwfs/w7tgBI1TBKm +# vlJb9aba4IsLSHfWhUfVELnG6Krui2otBVxgxrQqW5wjHF9F4xoUHm83yxkzgGqJ +# TaNqZmN4k9Uwz5UfAgMBAAGjggHjMIIB3zAQBgkrBgEEAYI3FQEEAwIBADAdBgNV +# HQ4EFgQU5vxfe7siAFjkck619CF0IzLm76wwGQYJKwYBBAGCNxQCBAweCgBTAHUA +# YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +# 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny +# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw +# MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov +# L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w +# Ni0yMy5jcnQwgZ0GA1UdIASBlTCBkjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUF +# BwIBFjFodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1 +# bHQuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5 +# AF8AUwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAadO9X +# Tyl7xBaFeLhQ0yL8CZ2sgpf4NP8qLJeVEuXkv8+/k8jjNKnbgbjcHgC+0jVvr+V/ +# eZV35QLU8evYzU4eG2GiwlojGvCMqGJRRWcI4z88HpP4MIUXyDlAptcOsyEp5aWh +# aYwik8x0mOehR0PyU6zADzBpf/7SJSBtb2HT3wfV2XIALGmGdj1R26Y5SMk3YW0H +# 3VMZy6fWYcK/4oOrD+Brm5XWfShRsIlKUaSabMi3H0oaDmmp19zBftFJcKq2rbty +# R2MX+qbWoqaG7KgQRJtjtrJpiQbHRoZ6GD/oxR0h1Xv5AiMtxUHLvx1MyBbvsZx/ +# /CJLSYpuFeOmf3Zb0VN5kYWd1dLbPXM18zyuVLJSR2rAqhOV0o4R2plnXjKM+zeF +# 0dx1hZyHxlpXhcK/3Q2PjJst67TuzyfTtV5p+qQWBAGnJGdzz01Ptt4FVpd69+lS +# TfR3BU+FxtgL8Y7tQgnRDXbjI1Z4IiY2vsqxjG6qHeSF2kczYo+kyZEzX3EeQK+Y +# Zcki6EIhJYocLWDZN4lBiSoWD9dhPJRoYFLv1keZoIBA7hWBdz6c4FMYGlAdOJWb +# HmYzEyc5F3iHNs5Ow1+y9T1HU7bg5dsLYT0q15IszjdaPkBCMaQfEAjCVpy/JF1R +# Ap1qedIX09rBlI4HeyVxRKsGaubUxt8jmpZ1xTGCFWYwghViAgEBMIGVMH4xCzAJ +# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k +# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv +# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTACEzMAAAMlSLKdDn/F9B8AAAAAAyUw +# DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK +# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIEopdEf3 +# vaw+bknrxifr7wCqVwSSQ1Hlz/MHRULiG6hkMEIGCisGAQQBgjcCAQwxNDAyoBSA +# EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w +# DQYJKoZIhvcNAQEBBQAEggEAEpHxxMJW5O3yToeAphpQ2yghxrlhVAzS2dfjpRso +# Ff8/2uCEJrO6/122ZwHnqU0L4/25t8wImECJ4vuzz3gooRNDyNWbReuyKfJU3zi3 +# 4unFzqvyCzGyQ/dvvhy0++Rcsjz0kqiPc3ZcTY7satfIjXgVKoRd4HUcR+uSm9S3 +# f8UebYaUQbg+9i/TsyNj7izv6Usa+j9PJrbXlxddfPKwrkL5u7UeQIyWkjtQAHJr +# /RtOiL9ytfuIr0vmmXYMM/V9hwQgI18jkW0loJLcIYoAqGIHIFIF9IVvEUnXV/w1 +# yLp4HULRA8qxuZFJqANE2j38qUnoaV74bfUHrFwCYrVrbKGCEvAwghLsBgorBgEE +# AYI3AwMBMYIS3DCCEtgGCSqGSIb3DQEHAqCCEskwghLFAgEDMQ8wDQYJYIZIAWUD +# BAIBBQAwggFUBgsqhkiG9w0BCRABBKCCAUMEggE/MIIBOwIBAQYKKwYBBAGEWQoD +# ATAxMA0GCWCGSAFlAwQCAQUABCACc8p2Hz3gsCbkAsFAcD4L7CYAHdk/XAD1NGfb +# J6p25QIGX7vQ8OVeGBIyMDIwMTIwMzE4MjI1OS41NVowBIACAfSggdSkgdEwgc4x +# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt +# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p +# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg +# VFNTIEVTTjo2MEJDLUUzODMtMjYzNTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt +# U3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABJt+6SyK5goIHAAAA +# AAEmMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo +# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y +# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw +# MB4XDTE5MTIxOTAxMTQ1OVoXDTIxMDMxNzAxMTQ1OVowgc4xCzAJBgNVBAYTAlVT +# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK +# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy +# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo2MEJD +# LUUzODMtMjYzNTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj +# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ4wvoacTvMNlXQTtfF/ +# Cx5Ol3X0fcjUNMvjLgTmO5+WHYJFbp725P3+qvFKDRQHWEI1Sz0gB24urVDIjXjB +# h5NVNJVMQJI2tltv7M4/4IbhZJb3xzQW7LolEoZYUZanBTUuyly9osCg4o5joViT +# 2GtmyxK+Fv5kC20l2opeaeptd/E7ceDAFRM87hiNCsK/KHyC+8+swnlg4gTOey6z +# QqhzgNsG6HrjLBuDtDs9izAMwS2yWT0T52QA9h3Q+B1C9ps2fMKMe+DHpG+0c61D +# 94Yh6cV2XHib4SBCnwIFZAeZE2UJ4qPANSYozI8PH+E5rCT3SVqYvHou97HsXvP2 +# I3MCAwEAAaOCARswggEXMB0GA1UdDgQWBBRJq6wfF7B+mEKN0VimX8ajNA5hQTAf +# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH +# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU +# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF +# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0 +# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG +# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQBAlvudaOlv9Cfzv56bnX41czF6tLtH +# LB46l6XUch+qNN45ZmOTFwLot3JjwSrn4oycQ9qTET1TFDYd1QND0LiXmKz9OqBX +# ai6S8XdyCQEZvfL82jIAs9pwsAQ6XvV9jNybPStRgF/sOAM/Deyfmej9Tg9FcRwX +# ank2qgzdZZNb8GoEze7f1orcTF0Q89IUXWIlmwEwQFYF1wjn87N4ZxL9Z/xA2m/R +# 1zizFylWP/mpamCnVfZZLkafFLNUNVmcvc+9gM7vceJs37d3ydabk4wR6ObR34sW +# aLppmyPlsI1Qq5Lu6bJCWoXzYuWpkoK6oEep1gML6SRC3HKVS3UscZhtMIIGcTCC +# BFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC +# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV +# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv +# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcN +# MjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv +# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 +# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIw +# DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0 +# VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEw +# RA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQe +# dGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKx +# Xf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4G +# kbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEA +# AaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7 +# fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC +# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX +# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v +# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI +# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0g +# AQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93 +# d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYB +# BQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUA +# bQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOh +# IW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS +# +7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlK +# kVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon +# /VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOi +# PPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/ +# fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCII +# YdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0 +# cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7a +# KLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQ +# cdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+ +# NR4Iuto229Nfj950iEkSoYIC0jCCAjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYT +# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD +# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBP +# cGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo2 +# MEJDLUUzODMtMjYzNTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy +# dmljZaIjCgEBMAcGBSsOAwIaAxUACmcyOWmZxErpq06B8dy6oMZ6//yggYMwgYCk +# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH +# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD +# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF +# AONzfakwIhgPMjAyMDEyMDMxOTA3MjFaGA8yMDIwMTIwNDE5MDcyMVowdzA9Bgor +# BgEEAYRZCgQBMS8wLTAKAgUA43N9qQIBADAKAgEAAgIoGgIB/zAHAgEAAgIRSTAK +# AgUA43TPKQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB +# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAKzB0KxZ21YSR5ZB +# ZfGfsSsMqo8gnv/Gk/hj12rv/vw2w7+81uMVuJKZM42NwYaFOqunHOi4mkrgnajr +# s5VoGdaAnR9tVYemCcfB8JpiRUXP/TaAIWagPWMV9N8W1KNhwD8oXjB6qccDDUOs +# ZCdrYduZ1r9NCsNftTTr0SAgQl0hMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMC +# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV +# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp +# bWUtU3RhbXAgUENBIDIwMTACEzMAAAEm37pLIrmCggcAAAAAASYwDQYJYIZIAWUD +# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B +# CQQxIgQgjA9rw54ROtynY8O9Hq5orWUG65LFOv3PIXfQQ9q8T2QwgfoGCyqGSIb3 +# DQEJEAIvMYHqMIHnMIHkMIG9BCA2/c/vnr1ecAzvapOWZ2xGfAkzrkfpGcrvMW07 +# CQl1DzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u +# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp +# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB +# Jt+6SyK5goIHAAAAAAEmMCIEIM0M3nvQo53PrGZJBLdCIo3uAE9rxn4U6vzR79cx +# I9L2MA0GCSqGSIb3DQEBCwUABIIBAF0hV7dAE0BWyO6J5UF+u2ETiZa0XepPEr9I +# nxT9YavBguRDer3IEslL/Lk78NLuO3MVrquuZpP9iIA++zn996XgmOHHn8JK7aKq +# ohJc/ujlibFqRHgyhT0aD+eDgNn7BRGcxCEjnOykis1WKi0GV3w8kZyKtK5wbGZ/ +# 5BxMkYA9Rf3DgWMz/5szdG3EFQ2+Tw4yBVoJzD39oF6wpg+2TOZIVX/wza4LN6sX +# 2mTkFAK8Lqai9/VnAwcdLAB7VmC+cOpxgldVaoUkDmCcTuaqDEwYJTlW0UNJJrMc +# K59IoC1gj4pQcPONu5sbV9HoU0NjurTlqcejedFm/VZJP7g4AqY= +# SIG # End signature block diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..b4fd74f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/cs-CZ/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/cs-CZ/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..358326f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/cs-CZ/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/de-DE/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/de-DE/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..cc7f64f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/de-DE/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/en-US/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/en-US/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..b4fd74f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/en-US/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/es-ES/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/es-ES/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..686c94c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/es-ES/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/fr-FR/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/fr-FR/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..c9d3806 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/fr-FR/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/it-IT/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/it-IT/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..8665e5e Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/it-IT/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ja-JP/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ja-JP/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..6f2305f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ja-JP/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ko-KR/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ko-KR/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..c70ec11 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ko-KR/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/pl-PL/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/pl-PL/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..a76541b Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/pl-PL/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/pt-BR/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/pt-BR/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..315a5db Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/pt-BR/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ru-RU/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ru-RU/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..75f7408 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/ru-RU/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/tr-TR/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/tr-TR/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..5d2efbb Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/tr-TR/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/zh-CN/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/zh-CN/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..1409591 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/zh-CN/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/zh-TW/Add-AppDevPackage.psd1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/zh-TW/Add-AppDevPackage.psd1 new file mode 100644 index 0000000..410ac8c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Add-AppDevPackage.resources/zh-TW/Add-AppDevPackage.psd1 differ diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Install.ps1 b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Install.ps1 new file mode 100644 index 0000000..49bd4ab --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Install.ps1 @@ -0,0 +1,229 @@ +# +# This script just calls the Add-AppDevPackage.ps1 script that lives next to it. +# + +param( + [switch]$Force = $false, + [switch]$SkipLoggingTelemetry = $false +) + +$scriptArgs = "" +if ($Force) +{ + $scriptArgs = '-Force' +} + +if ($SkipLoggingTelemetry) +{ + if ($Force) + { + $scriptArgs += ' ' + } + + $scriptArgs += '-SkipLoggingTelemetry' +} + +try +{ + # Log telemetry data regarding the use of the script if possible. + # There are three ways that this can be disabled: + # 1. If the "TelemetryDependencies" folder isn't present. This can be excluded at build time by setting the MSBuild property AppxLogTelemetryFromSideloadingScript to false + # 2. If the SkipLoggingTelemetry switch is passed to this script. + # 3. If Visual Studio telemetry is disabled via the registry. + $TelemetryAssembliesFolder = (Join-Path $PSScriptRoot "TelemetryDependencies") + if (!$SkipLoggingTelemetry -And (Test-Path $TelemetryAssembliesFolder)) + { + $job = Start-Job -FilePath (Join-Path $TelemetryAssembliesFolder "LogSideloadingTelemetry.ps1") -ArgumentList $TelemetryAssembliesFolder, "VS/DesignTools/SideLoadingScript/Install", $null, $null + Wait-Job -Job $job -Timeout 60 | Out-Null + } +} +catch +{ + # Ignore telemetry errors +} + +$currLocation = Get-Location +Set-Location $PSScriptRoot +Invoke-Expression ".\Add-AppDevPackage.ps1 $scriptArgs" +Set-Location $currLocation +# SIG # Begin signature block +# MIIhgwYJKoZIhvcNAQcCoIIhdDCCIXACAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC7kxV/l3biwCGH +# VuAKUAkPVeCZ2LSQIMJf+ROzV3B37KCCC3IwggT6MIID4qADAgECAhMzAAADJUiy +# nQ5/xfQfAAAAAAMlMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTAwHhcNMjAwMzA0MTgyOTI5WhcNMjEwMzAzMTgyOTI5WjB0MQsw +# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u +# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +# AQCjpRI2NHmdF4E+oz+32gQNFWfiWA/gW26xpPqf0l47t99p7IIKd5CuTAMePNYW +# XHST8pFfb8yaTNWz6nECabhQTCIxAqtAzVpCNWXiuQDe18eEUoUFN2sgoMhpU7gb +# 0gZigbhvznmT0moq7orBEAMcrW6C88+9JyqWBgDK0MBbpxjIwBv0uPgj3R40ItML +# Qw9Lb0SBnriOEPQKGDCO2AI6MSi++xe5YXOkQZrLCDc6Tl/f/fTzn1Ci+JR7YJMd +# dq8f2Ne42ogsUVIW6JH8SKbLQXb9xOVn4fMiG9b6PgRugApS0IKAUI8OQQ2kSr2a +# 1BsKEY9B7MNUeFBXB74OrutZAgMBAAGjggF5MIIBdTAfBgNVHSUEGDAWBgorBgEE +# AYI3PQYBBggrBgEFBQcDAzAdBgNVHQ4EFgQULcKPAJ0r4hUrTVSYmpa5RA+uHnww +# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 +# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzA4NjUrNDU4NDkzMB8GA1UdIwQYMBaAFOb8 +# X3u7IgBY5HJOtfQhdCMy5u+sMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu +# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY0NvZFNpZ1BDQV8yMDEw +# LTA3LTA2LmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93 +# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljQ29kU2lnUENBXzIwMTAtMDct +# MDYuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBAFxz4O+cWeBo +# 86e5EImiUeJXoJ5huJwH6l3YUBLhBt+t+uE6zDtBqmygeAq+qMs3otaucTmO6VEy +# LRACa7Yx8xxDLK7MAcnxwAY6SYjciErNsDf1tApeZkCIINFW/8S2QKMSQXf4OJol +# jWHo1TkniL9IRmzviN9l42NYNJB9i71ezxP+6ZN4PDWi8QVe70dGCLl9O2RxPQFh +# Ecl3jWdCu5C1FDRg6qMpcx3qseQR2QF4+d4EE/UQ1h3YeShbtuzxf0ksbBnQqVU2 +# ZJ9E/GJUTWUSsYxsJnG8xg3G46Jz3ttfVE3coMLKh1fHqsI3XXIlVzT3BIx3N9nL +# g18hwONtu5kwggZwMIIEWKADAgECAgphDFJMAAAAAAADMA0GCSqGSIb3DQEBCwUA +# MIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH +# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQD +# EylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0x +# MDA3MDYyMDQwMTdaFw0yNTA3MDYyMDUwMTdaMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpDmRQ +# eWe1xOP9CQBMnpSs91Zo6kTYz8VYT6mldnxtRbrTOZK0pB75+WWC5BfSj/1EnAjo +# ZZPOLFWEv30I4y4rqEErGLeiS25JTGsVB97R0sKJHnGUzbV/S7SvCNjMiNZrF5Q6 +# k84mP+zm/jSYV9UdXUn2siou1YW7WT/4kLQrg3TKK7M7RuPwRknBF2ZUyRy9HcRV +# Yldy+Ge5JSA03l2mpZVeqyiAzdWynuUDtWPTshTIwciKJgpZfwfs/w7tgBI1TBKm +# vlJb9aba4IsLSHfWhUfVELnG6Krui2otBVxgxrQqW5wjHF9F4xoUHm83yxkzgGqJ +# TaNqZmN4k9Uwz5UfAgMBAAGjggHjMIIB3zAQBgkrBgEEAYI3FQEEAwIBADAdBgNV +# HQ4EFgQU5vxfe7siAFjkck619CF0IzLm76wwGQYJKwYBBAGCNxQCBAweCgBTAHUA +# YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +# 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny +# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw +# MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov +# L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w +# Ni0yMy5jcnQwgZ0GA1UdIASBlTCBkjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUF +# BwIBFjFodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1 +# bHQuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5 +# AF8AUwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAadO9X +# Tyl7xBaFeLhQ0yL8CZ2sgpf4NP8qLJeVEuXkv8+/k8jjNKnbgbjcHgC+0jVvr+V/ +# eZV35QLU8evYzU4eG2GiwlojGvCMqGJRRWcI4z88HpP4MIUXyDlAptcOsyEp5aWh +# aYwik8x0mOehR0PyU6zADzBpf/7SJSBtb2HT3wfV2XIALGmGdj1R26Y5SMk3YW0H +# 3VMZy6fWYcK/4oOrD+Brm5XWfShRsIlKUaSabMi3H0oaDmmp19zBftFJcKq2rbty +# R2MX+qbWoqaG7KgQRJtjtrJpiQbHRoZ6GD/oxR0h1Xv5AiMtxUHLvx1MyBbvsZx/ +# /CJLSYpuFeOmf3Zb0VN5kYWd1dLbPXM18zyuVLJSR2rAqhOV0o4R2plnXjKM+zeF +# 0dx1hZyHxlpXhcK/3Q2PjJst67TuzyfTtV5p+qQWBAGnJGdzz01Ptt4FVpd69+lS +# TfR3BU+FxtgL8Y7tQgnRDXbjI1Z4IiY2vsqxjG6qHeSF2kczYo+kyZEzX3EeQK+Y +# Zcki6EIhJYocLWDZN4lBiSoWD9dhPJRoYFLv1keZoIBA7hWBdz6c4FMYGlAdOJWb +# HmYzEyc5F3iHNs5Ow1+y9T1HU7bg5dsLYT0q15IszjdaPkBCMaQfEAjCVpy/JF1R +# Ap1qedIX09rBlI4HeyVxRKsGaubUxt8jmpZ1xTGCFWcwghVjAgEBMIGVMH4xCzAJ +# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k +# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv +# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTACEzMAAAMlSLKdDn/F9B8AAAAAAyUw +# DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK +# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEILnmx/DL +# sme4EXSNG1kZGesXMcnWcLyJHy2/07NYyjrlMEIGCisGAQQBgjcCAQwxNDAyoBSA +# EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w +# DQYJKoZIhvcNAQEBBQAEggEAWf4xN5hBgLMPD+UvI9yd8Esbym1L89KoQR3Q6KYP +# D/k9LHlWgyynh9qPI+IHQBL/WUK/weE4HckF0Kp7sVEYP5+BdFXOqhL9d+yhlaje +# 8+TM/mhi8XEclFvYfoXTXwBucUZ7wIOReoY5xOXDgs4PxwPy9+B06vlwzNRQKe9x +# woGRDVWQOGgADUEwQIrfHaWIo81lYhWFr7FJYuzy41QL2dvqPtLLGcEKs+QaI9CJ +# OkYV5WGKVIXO0U8kRy4tZrXUGQzJcGjErYEDVo+g5C51G7rsnRjMy78lsFhU87mC +# ggsawvw65aWTaItywPv490JlCYlEbEaXDPiS+08+OL25gaGCEvEwghLtBgorBgEE +# AYI3AwMBMYIS3TCCEtkGCSqGSIb3DQEHAqCCEsowghLGAgEDMQ8wDQYJYIZIAWUD +# BAIBBQAwggFVBgsqhkiG9w0BCRABBKCCAUQEggFAMIIBPAIBAQYKKwYBBAGEWQoD +# ATAxMA0GCWCGSAFlAwQCAQUABCAFmHaXyBi0zvz+jM7341nn4CbCFH90wqKJKONQ +# oT7wZAIGX7vQ8OVmGBMyMDIwMTIwMzE4MjI1OS45OTlaMASAAgH0oIHUpIHRMIHO +# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk +# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBN +# aWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVz +# IFRTUyBFU046NjBCQy1FMzgzLTI2MzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1l +# LVN0YW1wIFNlcnZpY2Wggg5EMIIE9TCCA92gAwIBAgITMwAAASbfuksiuYKCBwAA +# AAABJjANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz +# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv +# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx +# MDAeFw0xOTEyMTkwMTE0NTlaFw0yMTAzMTcwMTE0NTlaMIHOMQswCQYDVQQGEwJV +# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE +# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3Bl +# cmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046NjBC +# Qy1FMzgzLTI2MzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZp +# Y2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCeML6GnE7zDZV0E7Xx +# fwseTpd19H3I1DTL4y4E5juflh2CRW6e9uT9/qrxSg0UB1hCNUs9IAduLq1QyI14 +# wYeTVTSVTECSNrZbb+zOP+CG4WSW98c0Fuy6JRKGWFGWpwU1LspcvaLAoOKOY6FY +# k9hrZssSvhb+ZAttJdqKXmnqbXfxO3HgwBUTPO4YjQrCvyh8gvvPrMJ5YOIEznsu +# s0Koc4DbBuh64ywbg7Q7PYswDMEtslk9E+dkAPYd0PgdQvabNnzCjHvgx6RvtHOt +# Q/eGIenFdlx4m+EgQp8CBWQHmRNlCeKjwDUmKMyPDx/hOawk90lamLx6Lvex7F7z +# 9iNzAgMBAAGjggEbMIIBFzAdBgNVHQ4EFgQUSausHxewfphCjdFYpl/GozQOYUEw +# HwYDVR0jBBgwFoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmg +# R4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWlj +# VGltU3RhUENBXzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEF +# BQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1T +# dGFQQ0FfMjAxMC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggr +# BgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAQEAQJb7nWjpb/Qn87+em51+NXMxerS7 +# RyweOpel1HIfqjTeOWZjkxcC6LdyY8Eq5+KMnEPakxE9UxQ2HdUDQ9C4l5is/Tqg +# V2oukvF3cgkBGb3y/NoyALPacLAEOl71fYzcmz0rUYBf7DgDPw3sn5no/U4PRXEc +# F2p5NqoM3WWTW/BqBM3u39aK3ExdEPPSFF1iJZsBMEBWBdcI5/OzeGcS/Wf8QNpv +# 0dc4sxcpVj/5qWpgp1X2WS5GnxSzVDVZnL3PvYDO73HibN+3d8nWm5OMEejm0d+L +# Fmi6aZsj5bCNUKuS7umyQlqF82LlqZKCuqBHqdYDC+kkQtxylUt1LHGYbTCCBnEw +# ggRZoAMCAQICCmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYT +# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD +# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS +# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoX +# DTI1MDcwMTIxNDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 +# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh +# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEi +# MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRr +# dFQQ1aUKAIKF++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmx +# MEQP8WCIhFRDDNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKE +# HnRhZ5FfgVSxz5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBi +# sV39dx898Fd1rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpO +# BpG2iAg16HgcsOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMB +# AAGjggHmMIIB4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPND +# e3xGG8UzaFqFbVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQD +# AgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb +# 186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29t +# L3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoG +# CCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQu +# Y29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1Ud +# IAEB/wSBlTCBkjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8v +# d3d3Lm1pY3Jvc29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsG +# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABl +# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2j +# oSFvs+umzPUxvs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJE +# Evu5U4zM9GASinbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5 +# SpFSAK84Dxf1L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJK +# J/1Vry/+tuWOM7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yj +# ojz6f32WapB4pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0 +# v35jWSUPei45V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgi +# CGHasFAeb73x4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iC +# tHLNHfS4hQEegPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO +# 2ii4sanblrKnQqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyX +# UHHXodLFVeNp3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWz +# fjUeCLraNtvTX4/edIhJEqGCAtIwggI7AgEBMIH8oYHUpIHRMIHOMQswCQYDVQQG +# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG +# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQg +# T3BlcmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046 +# NjBCQy1FMzgzLTI2MzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNl +# cnZpY2WiIwoBATAHBgUrDgMCGgMVAApnMjlpmcRK6atOgfHcuqDGev/8oIGDMIGA +# pH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT +# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE +# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQAC +# BQDjc32pMCIYDzIwMjAxMjAzMTkwNzIxWhgPMjAyMDEyMDQxOTA3MjFaMHcwPQYK +# KwYBBAGEWQoEATEvMC0wCgIFAONzfakCAQAwCgIBAAICKBoCAf8wBwIBAAICEUkw +# CgIFAON0zykCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgC +# AQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQCswdCsWdtWEkeW +# QWXxn7ErDKqPIJ7/xpP4Y9dq7/78NsO/vNbjFbiSmTONjcGGhTqrpxzouJpK4J2o +# 67OVaBnWgJ0fbVWHpgnHwfCaYkVFz/02gCFmoD1jFfTfFtSjYcA/KF4weqnHAw1D +# rGQna2Hbmda/TQrDX7U069EgIEJdITGCAw0wggMJAgEBMIGTMHwxCzAJBgNVBAYT +# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD +# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU +# aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABJt+6SyK5goIHAAAAAAEmMA0GCWCGSAFl +# AwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcN +# AQkEMSIEIFXetIvEvPtYV1qvJ6ZGZ3DuD3n/Bjp6d1DJUVCJnvwbMIH6BgsqhkiG +# 9w0BCRACLzGB6jCB5zCB5DCBvQQgNv3P7569XnAM72qTlmdsRnwJM65H6RnK7zFt +# OwkJdQ8wgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv +# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 +# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAA +# ASbfuksiuYKCBwAAAAABJjAiBCDNDN570KOdz6xmSQS3QiKN7gBPa8Z+FOr80e/X +# MSPS9jANBgkqhkiG9w0BAQsFAASCAQBRh1/GL+DGCxb5qotq9sUBwYDgra/6MNaz +# HIKwGmgDAg3mNsaSdkPiv1CllQzl/9I/V2/GnSikJi65MpIOD/+nEPHinHOXLVDR +# 4XwZtjLGCt41E3vETbHxsq6g2p77XDfnE7PtM8jzX/fe8MAfWciuBLGnRZX5o2ep +# 6IJa0fLKQVvM3tZ1ISpTvgtggNc845dNc7efjKONSm/ewGH3Pa6SVuOp5dyybHm0 +# WinYQza1XogkBrvSMYpmQbe3gAfzfq1EO9Nz4L1e8k/bm5At8rCEB+n/bc6TrMFE +# 2cF/z7jxwbDyzI3zE4d1S0HTEkvd59Y/5OjB4jH8YFkIrbhTRY9d +# SIG # End signature block diff --git a/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Snap.Hutao_1.17.1.0_x64.cer b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Snap.Hutao_1.17.1.0_x64.cer new file mode 100644 index 0000000..25bf82a Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/AppPackages/Snap.Hutao_1.17.1.0_x64_Test/Snap.Hutao_1.17.1.0_x64.cer differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/InAppLogo.png b/src/Snap.Hutao/Snap.Hutao/Assets/InAppLogo.png new file mode 100644 index 0000000..37ab18b Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/InAppLogo.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-100.png b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-100.png new file mode 100644 index 0000000..cb5aa28 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-100.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-125.png b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-125.png new file mode 100644 index 0000000..9e44e4f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-125.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-150.png b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-150.png new file mode 100644 index 0000000..7db2341 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-150.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-200.png b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-200.png new file mode 100644 index 0000000..aa55106 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-200.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-400.png b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-400.png new file mode 100644 index 0000000..f20d3bd Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/LargeTile.scale-400.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Logo.ico b/src/Snap.Hutao/Snap.Hutao/Assets/Logo.ico new file mode 100644 index 0000000..cd251c0 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Logo.ico differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-100.png b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-100.png new file mode 100644 index 0000000..d0234de Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-100.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-125.png b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-125.png new file mode 100644 index 0000000..6b3b76c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-125.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-150.png b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-150.png new file mode 100644 index 0000000..7c7f127 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-150.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-200.png b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-200.png new file mode 100644 index 0000000..cafd98d Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-200.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-400.png b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-400.png new file mode 100644 index 0000000..9d1ce40 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SmallTile.scale-400.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-100.png b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-100.png new file mode 100644 index 0000000..b15c22c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-100.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-125.png b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-125.png new file mode 100644 index 0000000..c519f6e Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-125.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-150.png b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-150.png new file mode 100644 index 0000000..3103a3e Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-150.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-200.png b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000..de9f5b3 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-200.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-400.png b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-400.png new file mode 100644 index 0000000..e1434fd Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/SplashScreen.scale-400.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-100.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-100.png new file mode 100644 index 0000000..d89d25e Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-100.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-125.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-125.png new file mode 100644 index 0000000..6956b64 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-125.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-150.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-150.png new file mode 100644 index 0000000..a0fdedb Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-150.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-200.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..ad2da47 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-200.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-400.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-400.png new file mode 100644 index 0000000..9ee6696 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square150x150Logo.scale-400.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000..0f340d4 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000..79a3788 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000..cd6e27d Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000..f1c22da Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000..bdf916b Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000..0f340d4 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-24.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-24.png new file mode 100644 index 0000000..79a3788 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000..cd6e27d Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000..f1c22da Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000..bdf916b Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-100.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-100.png new file mode 100644 index 0000000..4146c8e Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-100.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-125.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-125.png new file mode 100644 index 0000000..8058f9f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-125.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-150.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-150.png new file mode 100644 index 0000000..b3db61c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-150.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-200.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..8e95e3b Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-200.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-400.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-400.png new file mode 100644 index 0000000..07c87dc Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.scale-400.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-16.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000..0f340d4 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-16.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-24.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000..79a3788 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-24.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-256.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000..cd6e27d Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-256.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-32.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000..f1c22da Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-32.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-48.png b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000..bdf916b Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Square44x44Logo.targetsize-48.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.backup.png b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.backup.png new file mode 100644 index 0000000..152a109 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.backup.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-100.png b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-100.png new file mode 100644 index 0000000..166c56c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-100.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-125.png b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-125.png new file mode 100644 index 0000000..48ce382 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-125.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-150.png b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-150.png new file mode 100644 index 0000000..d55e06b Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-150.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-200.png b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-200.png new file mode 100644 index 0000000..fb4c5eb Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-200.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-400.png b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-400.png new file mode 100644 index 0000000..d6d2437 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/StoreLogo.scale-400.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-100.png b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000..5ac338c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-100.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-125.png b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000..f4cb923 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-125.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-150.png b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000..fa25129 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-150.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-200.png b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..b15c22c Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-400.png b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000..de9f5b3 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Assets/Wide310x150Logo.scale-400.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/BannedSymbols.txt b/src/Snap.Hutao/Snap.Hutao/BannedSymbols.txt new file mode 100644 index 0000000..12986aa --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/BannedSymbols.txt @@ -0,0 +1,5 @@ +// https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md +M:System.Collections.Generic.List`1.ForEach(System.Action{`0}) +M:Microsoft.UI.Xaml.Controls.ContentDialog.ShowAsync +M:Microsoft.UI.Xaml.Controls.ContentDialog.ShowAsync(Microsoft.UI.Xaml.Controls.ContentDialogPlacement) +P:Microsoft.UI.Xaml.UIElement.RasterizationScale \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Bootstrap.cs b/src/Snap.Hutao/Snap.Hutao/Bootstrap.cs new file mode 100644 index 0000000..d95e086 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Bootstrap.cs @@ -0,0 +1,111 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Snap.Hutao.Core; +using Snap.Hutao.Core.Logging; +using Snap.Hutao.Core.Security.Principal; +using Snap.Hutao.Win32; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Security.AccessControl; +using WinRT; + +[assembly: DisableRuntimeMarshalling] + +namespace Snap.Hutao; + +[SuppressMessage("", "SH001")] +public static partial class Bootstrap +{ + private const string LockName = "SNAP_HUTAO_BOOTSTRAP_LOCK"; + private static readonly ApplicationInitializationCallback AppInitializationCallback = InitializeApp; + private static Mutex? mutex; + + internal static void UseNamedPipeRedirection() + { + Debug.Assert(mutex is not null); + DisposableMarshal.DisposeAndClear(ref mutex); + } + + [STAThread] + private static void Main(string[] args) + { + if (Mutex.TryOpenExisting(LockName, out _)) + { + return; + } + + try + { + MutexSecurity mutexSecurity = new(); + mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow)); + mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity); + Debug.Assert(created); + } + catch (WaitHandleCannotBeOpenedException) + { + return; + } + + // Although we 'using' mutex there, the actual disposal is done in AppActivation + // The using is just to ensure we dispose the mutex when the application exits + using (mutex) + { + if (!OSPlatformSupported()) + { + return; + } + + Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000"); + Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128"); + AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false); + + ComWrappersSupport.InitializeComWrappers(); + + // By adding the using statement, we can dispose the injected services when closing + using (ServiceProvider serviceProvider = DependencyInjection.Initialize()) + { + Thread.CurrentThread.Name = "Snap Hutao Application Main Thread"; + + // If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging + // You can delete bin and obj folder and then rebuild. + // In a Desktop app this runs a message pump internally, + // and does not return until the application shuts down. + Application.Start(AppInitializationCallback); + XamlApplicationLifetime.Exited = true; + } + + SentrySdk.Flush(); + } + } + + private static void InitializeApp(ApplicationInitializationCallbackParams param) + { + Gen2GcCallback.Register(() => + { + SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime")); + return true; + }); + + IServiceProvider serviceProvider = Ioc.Default; + + _ = serviceProvider.GetRequiredService(); + _ = serviceProvider.GetRequiredService(); + } + + private static bool OSPlatformSupported() + { + if (!HutaoNative.Instance.IsCurrentWindowsVersionSupported()) + { + const string Message = """ + Snap Hutao 无法在版本低于 10.0.19045.5371 的 Windows 上运行,请更新系统。 + Snap Hutao cannot run on Windows versions earlier than 10.0.19045.5371. Please update your system. + """; + HutaoNative.Instance.ShowErrorMessage("Warning | 警告", Message); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt b/src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt new file mode 100644 index 0000000..2a7c119 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt @@ -0,0 +1 @@ +CA1501: 8 \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/BuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/BuilderExtension.cs new file mode 100644 index 0000000..818d3b0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/BuilderExtension.cs @@ -0,0 +1,61 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Diagnostics; + +namespace Snap.Hutao.Core.Abstraction; + +internal static class BuilderExtension +{ + extension(T builder) + where T : class, IBuilder + { + [DebuggerStepThrough] + public T Configure(Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + configure(builder); + return builder; + } + + [DebuggerStepThrough] + public unsafe T Configure(delegate* configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + configure(builder); + return builder; + } + + [DebuggerStepThrough] + public T If(bool condition, Action action) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(action); + + if (condition) + { + action(builder); + } + + return builder; + } + + [DebuggerStepThrough] + public unsafe T If(bool condition, delegate* action) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(action); + + if (condition) + { + action(builder); + } + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IBuilder.cs new file mode 100644 index 0000000..ec5ab51 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IBuilder.cs @@ -0,0 +1,6 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Abstraction; + +internal interface IBuilder; \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IDeconstruct.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IDeconstruct.cs new file mode 100644 index 0000000..185c8dd --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IDeconstruct.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Abstraction; + +internal interface IDeconstruct +{ + void Deconstruct(out T1 t1, out T2 t2); +} + +internal interface IDeconstruct +{ + void Deconstruct(out T1 t1, out T2 t2, out T3 t3); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IPinnable.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IPinnable.cs new file mode 100644 index 0000000..6c31f26 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IPinnable.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Abstraction; + +internal interface IPinnable +{ + ref TData GetPinnableReference(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IResurrectable.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IResurrectable.cs new file mode 100644 index 0000000..65c3d16 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IResurrectable.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Abstraction; + +internal interface IResurrectable +{ + void Resurrect(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/ITypeName.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/ITypeName.cs new file mode 100644 index 0000000..a5e18b1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/ITypeName.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Abstraction; + +internal interface ITypeName +{ + string TypeName { get; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/LimitedAccessFeatures.cs b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/LimitedAccessFeatures.cs new file mode 100644 index 0000000..113993c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/LimitedAccessFeatures.cs @@ -0,0 +1,85 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Frozen; +using System.Security.Cryptography; +using System.Text; +using Windows.ApplicationModel; + +namespace Snap.Hutao.Core.ApplicationModel; + +internal static class LimitedAccessFeatures +{ + private static readonly string PackagePublisherId = Package.Current.Id.PublisherId; + private static readonly string PackageFamilyName = Package.Current.Id.FamilyName; + + private static readonly FrozenDictionary Features = WinRTAdaptive.ToFrozenDictionary( + [ + KeyValuePair.Create("com.microsoft.services.cortana.cortanaactionableinsights_v1", "nEVyyzytE6ankNk1CIAu6sZsh8vKLw3Q7glTOHB11po="), + KeyValuePair.Create("com.microsoft.windows.additional_foreground_boost_processes", "c0abf97e-e14d-468a-b6bc-35be5c62610a"), + + // Windows Conversational Agent API + KeyValuePair.Create("com.microsoft.windows.applicationmodel.conversationalagent_v1", "hhrovbOc/z8TgeoWheL4RF5vLLJrKNAQpdyvhlTee6I"), + KeyValuePair.Create("com.microsoft.windows.applicationmodel.phonelinetransportdevice_v1", "cb9WIvVfhp+8lFhaSrB6V6zUBGqctteKi/f/9AIeoZ4"), + + // Application Window API + KeyValuePair.Create("com.microsoft.windows.applicationwindow", "e5a85131-319b-4a56-9577-1c1d9c781218"), + + // Shell, Window Focus API + KeyValuePair.Create("com.microsoft.windows.focussessionmanager.1", "ba3faac1-0878-4bb9-9b35-2224aa0ee7cf"), + + // Location Override APIs + KeyValuePair.Create("com.microsoft.windows.geolocationprovider", "6D1544E3-55CB-40D2-A022-31F24E139708"), + + // Dual Engine Interface API + KeyValuePair.Create("com.microsoft.windows.internetexplorer.iemode", "33951EE6-0B59-40EC-90D6-76B019316C16"), + + // Phi Silica API + KeyValuePair.Create("com.microsoft.windows.modelcontextprotocolservercatalog", "DC76200A35E1543A3F4E64D8267833BBD88E583FACAC160B860BC813D26EAFEF"), + + // Remote App Windowing APIs + KeyValuePair.Create("com.microsoft.windows.remote_app_windowing_apis", "f86b14bc-8690-4206-9101-72847823d265"), + + // Rich Edit Math API + KeyValuePair.Create("com.microsoft.windows.richeditmath", "RDZCQjY2M0YtQkFDMi00NkIwLUI3NzEtODg4NjMxMEVENkFF"), + + // Share Window Command API + KeyValuePair.Create("com.microsoft.windows.shell.sharewindowcommandsource_v1", "yDvrila5HS/y8SctohQM3WJZOby8NbSoL2hEPTyIRco="), + KeyValuePair.Create("com.microsoft.windows.storageprovidersuggestionshandler_v1", "BGoeg9Bd5WID7YZ84xr4V37w4d0pOues5QHpAm3krJw="), + + // Remote Desktop Provider API + KeyValuePair.Create("com.microsoft.windows.system.remotedesktop.provider_v1", "2F712169EF57A9FB0D590593743819F5F47E2DD13E4D9A5458DDA77608CC5E10"), + + // TaskbarManager Pinning API + KeyValuePair.Create("com.microsoft.windows.taskbar.pin", "4096B239A7295B635C090E647E867B5707DA6AB6CB78340B01FE4E0C8F4953D4"), + + // TaskbarManager Pinning API (Secondary Tile) + KeyValuePair.Create("com.microsoft.windows.taskbar.requestPinSecondaryTile", "04c19204-10d9-450a-95c4-2910c8f72be3"), + KeyValuePair.Create("com.microsoft.windows.textinputmethod", "QUYxMTREMjY2QUIwRTE0RkU3NTQ4QTRENjJFMTVDMkUxNjlDQjY1MDg3MEZGMDc1NTI0Nzg5Njk3NkQ0NkQzQw=="), + + // Toast Occlusion Manager API + KeyValuePair.Create("com.microsoft.windows.ui.notifications.preview.toastOcclusionManagerPreview", "738a6acf-45c1-44ed-85a4-5eb11dc2d084"), + + // Update Orchestrator API + KeyValuePair.Create("com.microsoft.windows.updateorchestrator.1", "20C662033A4007A55375BF00D986C280B41A418F"), + + // Window Decoration API + KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717") + ]); + + public static LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId) + { + return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId)); + } + + private static string GetToken(string featureId) + { + byte[] source = Encoding.UTF8.GetBytes($"{featureId}!{Features[featureId]}!{PackageFamilyName}"); + return Convert.ToBase64String(CryptographicOperations.HashData(HashAlgorithmName.SHA256, source).AsSpan(0, 16)); + } + + private static string GetAttestation(string featureId) + { + return $"{PackagePublisherId} has registered their use of {featureId} with Microsoft and agrees to the terms of use."; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs new file mode 100644 index 0000000..303fce6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs @@ -0,0 +1,18 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Snap.Hutao.Core.IO; + +namespace Snap.Hutao.Core.Caching; + +internal interface IImageCache +{ + ValueFile GetFileFromCategoryAndName(string category, string fileName); + + ValueTask GetFileFromCacheAsync(Uri uri); + + ValueTask GetFileFromCacheAsync(Uri uri, ElementTheme theme); + + void Remove(Uri uriForCachedItem); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCacheDownloadOperation.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCacheDownloadOperation.cs new file mode 100644 index 0000000..dbdb2b2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCacheDownloadOperation.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.IO; + +namespace Snap.Hutao.Core.Caching; + +internal interface IImageCacheDownloadOperation +{ + ValueTask DownloadFileAsync(Uri uri, ValueFile baseFile); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs new file mode 100644 index 0000000..9096235 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs @@ -0,0 +1,145 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Core.IO; +using Snap.Hutao.Core.Logging; +using Snap.Hutao.Factory.Process; +using Snap.Hutao.Web.Endpoint.Hutao; +using Snap.Hutao.Win32; +using System.Collections.Frozen; +using System.Diagnostics; +using System.IO; +using ThemeFile = (Microsoft.UI.Xaml.ElementTheme, Snap.Hutao.Core.IO.ValueFile); + +namespace Snap.Hutao.Core.Caching; + +[Service(ServiceLifetime.Singleton, typeof(IImageCache))] +internal sealed partial class ImageCache : IImageCache +{ + private static readonly FrozenSet SupportedSchemes = + [ + Uri.UriSchemeHttp, + Uri.UriSchemeHttps, + ]; + + private readonly AsyncKeyedLock themeFileLocks = new(); + private readonly AsyncKeyedLock downloadLocks = new(); + + private readonly IImageCacheDownloadOperation downloadOperation; + + [GeneratedConstructor] + public partial ImageCache(IServiceProvider serviceProvider); + + private string CacheFolder + { + get => LazyInitializer.EnsureInitialized(ref field, static () => + { + try + { + string folder = HutaoRuntime.GetLocalCacheImageCacheDirectory(); + Directory.CreateDirectory(Path.Combine(folder, "Light")); + Directory.CreateDirectory(Path.Combine(folder, "Dark")); + return folder; + } + catch (Exception ex) + { + // 0x80070570 ERROR_FILE_CORRUPT + HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex)); + ProcessFactory.KillCurrent(); + return string.Empty; + } + }); + } + + public void Remove(Uri uriForCachedItem) + { + try + { + File.Delete(ImageCacheFile.GetHashedFile(CacheFolder, uriForCachedItem)); + } + catch + { + // Ignored + } + } + + public ValueTask GetFileFromCacheAsync(Uri uri) + { + return GetFileFromCacheAsync(uri, ElementTheme.Default); + } + + public async ValueTask GetFileFromCacheAsync(Uri uri, ElementTheme theme) + { + Debug.Assert(SupportedSchemes.Contains(uri.Scheme), "Unsupported URI scheme"); + + ImageCacheFile imageCacheFile = ImageCacheFile.Create(CacheFolder, uri); + string themedFileFullPath = imageCacheFile.GetThemedFile(theme); + + using (await themeFileLocks.LockAsync((theme, imageCacheFile.HashedFileName)).ConfigureAwait(false)) + { + if (IsFileInvalid(themedFileFullPath)) + { + using (await downloadLocks.LockAsync(imageCacheFile.HashedFileName).ConfigureAwait(false)) + { + if (IsFileInvalid(imageCacheFile.DefaultFilePath)) + { + SentrySdk.AddBreadcrumb(BreadcrumbFactory2.CreateInfo("Begin to download file", "Core.Caching.ImageCache", [("Uri", uri.ToString()), ("File", imageCacheFile.DefaultFilePath)])); + + try + { + await downloadOperation.DownloadFileAsync(uri, imageCacheFile.DefaultFilePath).ConfigureAwait(false); + } + catch (Exception) + { + Remove(uri); + throw; + } + } + } + } + + await EnsureThemedMonochromeFileExistsAsync(imageCacheFile, theme).ConfigureAwait(false); + return themedFileFullPath; + } + } + + public ValueFile GetFileFromCategoryAndName(string category, string fileName) + { + return ImageCacheFile.Create(CacheFolder, StaticResourcesEndpoints.StaticRaw(category, fileName)).DefaultFilePath; + } + + private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true) + { + if (!File.Exists(file)) + { + return treatNullFileAsInvalid; + } + + return new FileInfo(file).Length == 0; + } + + private static async ValueTask EnsureThemedMonochromeFileExistsAsync(ImageCacheFile imageCacheFile, ElementTheme theme) + { + if (theme is ElementTheme.Default) + { + return; + } + + try + { + using (FileStream sourceStream = File.OpenRead(imageCacheFile.DefaultFilePath)) + { + using (FileStream themeStream = File.Create(imageCacheFile.GetThemedFile(theme))) + { + await MonoChromeImageConverter.ConvertAndCopyToAsync(theme, sourceStream, themeStream).ConfigureAwait(false); + } + } + } + catch (IOException ex) + { + throw InternalImageCacheException.Throw("Failed to convert image", ex); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCacheDownloadOperation.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCacheDownloadOperation.cs new file mode 100644 index 0000000..ffb2f30 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCacheDownloadOperation.cs @@ -0,0 +1,118 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Core.IO; +using Snap.Hutao.ViewModel.Guide; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Mime; + +namespace Snap.Hutao.Core.Caching; + +[Service(ServiceLifetime.Singleton, typeof(IImageCacheDownloadOperation))] +[HttpClient(HttpClientConfiguration.Default)] +[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)] +internal sealed partial class ImageCacheDownloadOperation : IImageCacheDownloadOperation +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly IHttpClientFactory httpClientFactory; + + [GeneratedConstructor] + public partial ImageCacheDownloadOperation(IServiceProvider serviceProvider); + + public async ValueTask DownloadFileAsync(Uri uri, ValueFile baseFile) + { + using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCacheDownloadOperation))) + { + try + { + await DownloadFileUsingHttpClientAsync(httpClient, uri, baseFile).ConfigureAwait(false); + } + catch (HttpRequestException) + { + // Ignore + } + } + + if (!File.Exists(baseFile)) + { + throw InternalImageCacheException.Throw($"'{uri.OriginalString}': File not exists after download attempt."); + } + } + + private async ValueTask DownloadFileUsingHttpClientAsync(HttpClient httpClient, Uri uri, ValueFile baseFile) + { + HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory + .Create() + .SetRequestUri(uri) + .SetStaticResourceControlHeadersIfRequired() + .Get(); + + using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage) + { + using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) + { + if (!responseMessage.IsSuccessStatusCode) + { + throw responseMessage.StatusCode switch + { + HttpStatusCode.NotFound => InternalImageCacheException.Throw($"Unable to download file from '{uri.OriginalString}': 404 Not Found"), + _ => InternalImageCacheException.Throw($"Unexpected HTTP status code {responseMessage.StatusCode}"), + }; + } + + if (responseMessage.Content.Headers.ContentType?.MediaType is MediaTypeNames.Application.Json) + { + InternalImageCacheException.Throw($"Unexpected content type: {MediaTypeNames.Application.Json}"); + } + + using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + string? directoryName = Path.GetDirectoryName(baseFile); + ArgumentException.ThrowIfNullOrEmpty(directoryName); + + try + { + Directory.CreateDirectory(directoryName); + } + catch (DirectoryNotFoundException dnfEx) + { + throw InternalImageCacheException.Throw($"Unable to create folder at '{directoryName}'", dnfEx); + } + + FileStream fileStream; + try + { + fileStream = File.Create(baseFile); + } + catch (IOException ex) + { + // The process cannot access the file '?' because it is being used by another process. + throw InternalImageCacheException.Throw($"Unable to create file at '{baseFile}'", ex); + } + + try + { + using (fileStream) + { + await httpStream.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + catch (IOException ex) + { + // Received an unexpected EOF or 0 bytes from the transport stream. + // Unable to read data from the transport connection: 远程主机强迫关闭了一个现有的连接。. SocketException: ConnectionReset + // Unable to read data from the transport connection: 你的主机中的软件中止了一个已建立的连接。. SocketException: ConnectionAborted + // HttpIOException: The response ended prematurely. (ResponseEnded) + // 磁盘空间不足。 : '?'. + throw InternalImageCacheException.Throw("Unable to copy stream content to file", ex); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCacheFile.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCacheFile.cs new file mode 100644 index 0000000..47d954d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCacheFile.cs @@ -0,0 +1,61 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Snap.Hutao.Core.IO; +using Snap.Hutao.Core.IO.Hashing; +using System.IO; +using System.Security.Cryptography; + +namespace Snap.Hutao.Core.Caching; + +internal sealed class ImageCacheFile +{ + private readonly string directory; + + private ImageCacheFile(ValueDirectory directory, string hashedFileName) + { + this.directory = directory; + HashedFileName = hashedFileName; + } + + public string HashedFileName { get; } + + [field: MaybeNull] + public string DefaultFilePath + { + get => field ??= Path.GetFullPath(Path.Combine(directory, HashedFileName)); + } + + public static ImageCacheFile Create(ValueDirectory folder, string url) + { + return new(folder, GetHashedFileName(url)); + } + + public static ImageCacheFile Create(ValueDirectory folder, Uri uri) + { + return new(folder, GetHashedFileName(uri.OriginalString)); + } + + public static ValueFile GetHashedFile(ValueDirectory folder, string url) + { + return Path.GetFullPath(Path.Combine(folder, GetHashedFileName(url))); + } + + public static ValueFile GetHashedFile(ValueDirectory folder, Uri uri) + { + return Path.GetFullPath(Path.Combine(folder, GetHashedFileName(uri.OriginalString))); + } + + public static string GetHashedFileName(string url) + { + return Hash.ToHexString(HashAlgorithmName.SHA1, url); + } + + public ValueFile GetThemedFile(ElementTheme theme) + { + return theme is ElementTheme.Default + ? DefaultFilePath + : Path.GetFullPath(Path.Combine(directory, $"{theme}", HashedFileName)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/InternalImageCacheException.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/InternalImageCacheException.cs new file mode 100644 index 0000000..5c19fa8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/InternalImageCacheException.cs @@ -0,0 +1,29 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.ExceptionService; + +namespace Snap.Hutao.Core.Caching; + +internal class InternalImageCacheException : Exception, IInternalException +{ + private InternalImageCacheException(string message, Exception innerException) + : base(message, innerException) + { + } + + private InternalImageCacheException(string message) + : base(message) + { + } + + public static InternalImageCacheException Throw(string message, Exception innerException) + { + throw new InternalImageCacheException(message, innerException); + } + + public static InternalImageCacheException Throw(string message) + { + throw new InternalImageCacheException(message); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/MonoChromeImageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/MonoChromeImageConverter.cs new file mode 100644 index 0000000..11a9066 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/MonoChromeImageConverter.cs @@ -0,0 +1,47 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Snap.Hutao.UI; +using Snap.Hutao.Win32.System.WinRT; +using System.Diagnostics; +using System.IO; +using Windows.Foundation; +using Windows.Graphics.Imaging; +using WinRT; + +namespace Snap.Hutao.Core.Caching; + +internal static class MonoChromeImageConverter +{ + public static async ValueTask ConvertAndCopyToAsync(ElementTheme theme, Stream source, Stream destination) + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + + BitmapDecoder decoder = await BitmapDecoder.CreateAsync(source.AsRandomAccessStream()); + + // Always premultiplied to prevent some channels have a non-zero value when the alpha channel is zero + using (SoftwareBitmap sourceBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied)) + { + using (BitmapBuffer sourceBuffer = sourceBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite)) + { + using (IMemoryBufferReference reference = sourceBuffer.CreateReference()) + { + byte value = (byte)(theme is ElementTheme.Light ? 0x00 : 0xFF); + Debug.Assert(Thread.CurrentThread.IsBackground); + + reference.As().GetBuffer(out Span span); + foreach (ref Rgba32 pixel in span) + { + pixel.A = (byte)pixel.Luminance255; + pixel.R = pixel.G = pixel.B = value; + } + } + } + + BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, destination.AsRandomAccessStream()); + encoder.SetSoftwareBitmap(sourceBitmap); + await encoder.FlushAsync(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/DelegatingPropertyComparer.cs b/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/DelegatingPropertyComparer.cs new file mode 100644 index 0000000..2c08441 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/DelegatingPropertyComparer.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using JetBrains.Annotations; + +namespace Snap.Hutao.Core.Collection.Generic; + +internal abstract class DelegatingPropertyComparer : IComparer + where T : class +{ + private readonly IComparer delegatedComparer; + private readonly Func delegation; + + protected DelegatingPropertyComparer([RequireStaticDelegate] Func delegation, IComparer delegatedComparer) + { + this.delegation = delegation; + this.delegatedComparer = delegatedComparer; + } + + public int Compare(T? x, T? y) + { + return (x, y) switch + { + (null, not null) => -1, + (not null, null) => 1, + (null, null) => 0, + (not null, not null) => delegatedComparer.Compare(delegation(x), delegation(y)), + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/ImmutableArrayEnumeratorNoThrow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/ImmutableArrayEnumeratorNoThrow.cs new file mode 100644 index 0000000..6f7cc57 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/ImmutableArrayEnumeratorNoThrow.cs @@ -0,0 +1,37 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections; +using System.Collections.Immutable; + +namespace Snap.Hutao.Core.Collection.Generic; + +internal sealed class ImmutableArrayEnumeratorNoThrow : IEnumerator +{ + private readonly ImmutableArray array; + private int index; + + public ImmutableArrayEnumeratorNoThrow(ImmutableArray array) + { + this.array = array; + index = -1; + } + + public T Current { get => index < array.Length ? array[index] : default!; } + + object? IEnumerator.Current { get => Current; } + + public bool MoveNext() + { + return ++index < array.Length; + } + + public void Reset() + { + index = -1; + } + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/TwoEnumerableEnumerator.cs b/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/TwoEnumerableEnumerator.cs new file mode 100644 index 0000000..e3c8427 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Collection/Generic/TwoEnumerableEnumerator.cs @@ -0,0 +1,46 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using JetBrains.Annotations; +using System.Collections.Immutable; + +namespace Snap.Hutao.Core.Collection.Generic; + +internal sealed partial class TwoEnumerableEnumerator : IDisposable +{ + private readonly IEnumerator firstEnumerator; + private readonly IEnumerator secondEnumerator; + + public TwoEnumerableEnumerator(IEnumerable firstEnumerable, IEnumerable secondEnumerable) + { + firstEnumerator = GetNoThrowEnumeratorIfPossible(firstEnumerable); + secondEnumerator = GetNoThrowEnumeratorIfPossible(secondEnumerable); + } + + public (TFirst? First, TSecond? Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); } + + public bool MoveNext(ref bool moveFirst, ref bool moveSecond) + { + moveFirst = moveFirst && firstEnumerator.MoveNext(); + moveSecond = moveSecond && secondEnumerator.MoveNext(); + + return moveFirst || moveSecond; + } + + public void Dispose() + { + firstEnumerator.Dispose(); + secondEnumerator.Dispose(); + } + + [MustDisposeResource] + private static IEnumerator GetNoThrowEnumeratorIfPossible(IEnumerable enumerable) + { + if (enumerable is ImmutableArray immutableArray) + { + return new ImmutableArrayEnumeratorNoThrow(immutableArray); + } + + return enumerable.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CommandLineArgumentPrefix.cs b/src/Snap.Hutao/Snap.Hutao/Core/CommandLineArgumentPrefix.cs new file mode 100644 index 0000000..a73331f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/CommandLineArgumentPrefix.cs @@ -0,0 +1,10 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core; + +internal enum CommandLineArgumentPrefix +{ + WhiteSpace, + Equal, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CommandLineBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Core/CommandLineBuilder.cs new file mode 100644 index 0000000..f0f3019 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/CommandLineBuilder.cs @@ -0,0 +1,73 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text; + +namespace Snap.Hutao.Core; + +internal sealed class CommandLineBuilder +{ + private const char WhiteSpace = ' '; + + private readonly List