79 Commits
3.6.0 ... 5.4.0

Author SHA1 Message Date
HolographicHat
d08ac17d10 bump version 2025-04-13 03:14:07 +08:00
HolographicHat
980a47bf43 spectre console 2025-04-11 23:57:07 +08:00
HolographicHat
0e7be25b23 allow copy cocogoat import url 2025-04-09 01:10:21 +08:00
HolographicHat
4b052cf6c7 refactor CacheFile 2025-04-09 01:09:47 +08:00
HolographicHat
4ff2b454f3 refactor updater and injector 2025-04-08 23:15:53 +08:00
HolographicHat
f718687b3f [no ci] update feedback group id 2025-04-06 15:15:36 +08:00
HolographicHat
9915b9246e disable CETCompat 2025-04-05 21:47:59 +08:00
HolographicHat
e25bc9aeba Merge pull request #133 from BTMuli/master
✏️ Typo uigf to uiaf
2025-04-01 15:50:28 +08:00
目棃
08dd6eca76 ✏️ Typo uigf to uiaf 2025-04-01 15:21:04 +08:00
HolographicHat
acd2ccd803 Merge pull request #132 from Lightczx/master
Inject side check ignore case
2025-03-28 11:15:43 +08:00
DismissedLight
13fda3ba12 bump nuspec version 2025-03-28 10:59:25 +08:00
DismissedLight
1c821620bf Inject side check ignore case 2025-03-28 10:58:39 +08:00
HolographicHat
8a39ad0a77 [skip ci] update readme 2025-03-27 16:24:00 +08:00
HolographicHat
9d42141258 bump lib version 2025-03-26 16:22:59 +08:00
HolographicHat
2a91656b2e [skip ci] update ci 2025-03-26 16:19:42 +08:00
HolographicHat
602cf06a8b bump version 2025-03-26 16:03:26 +08:00
HolographicHat
c87b8c976d update 2025-03-26 16:02:48 +08:00
HolographicHat
099c22e9e7 Merge pull request #130 from 34736384/master
update resolver
2025-03-26 15:00:05 +08:00
REL
f1fe6e1f9e update resolver 2025-03-26 02:45:33 -04:00
HolographicHat
fb0a46480f [no ci] bump version 2025-02-12 17:35:57 +08:00
HolographicHat
7b413384c3 Merge pull request #129 from 34736384/master
fix player store id resolver
2025-02-12 17:27:43 +08:00
REL
e251497edc fix player store id resolver 2025-02-12 04:24:43 -05:00
HolographicHat
b1f5d9a2b2 [skip ci] Merge pull request #128 from qhy040404/nuget 2025-01-27 21:14:55 +08:00
qhy040404
36a5b9a0e7 fix ci 2025-01-27 21:13:19 +08:00
HolographicHat
b0b70d585a [skip_ci] Merge pull request #127 from qhy040404/nuget 2025-01-27 21:04:42 +08:00
qhy040404
b9ab326d72 workflow_dispatch 2025-01-27 20:02:21 +08:00
qhy040404
5f210d4236 publish lib to nuget 2025-01-27 19:36:08 +08:00
HolographicHat
826063da60 [skip ci] Merge pull request #126 from qhy040404/patch 2025-01-26 13:43:33 +08:00
qhy040404
08697941d3 print cocogoat url 2025-01-26 12:02:39 +08:00
HolographicHat
e7d21865c7 [skip ci] support virtual item 2025-01-20 17:32:47 +08:00
HolographicHat
35773f49f4 [skip ci] Merge pull request #125 from Lightczx/master 2025-01-20 14:45:18 +08:00
DismissedLight
c4856c821f Fix inject side check 2025-01-20 11:08:04 +08:00
HolographicHat
6f1168e61e [skip ci] Merge pull request #124 from qhy040404/windowhook 2025-01-17 22:13:43 +08:00
qhy040404
d1bd0c7d7b fix 2025-01-17 21:55:50 +08:00
qhy040404
95ad187015 add pin to avoid unload 2025-01-17 21:40:59 +08:00
HolographicHat
6c0264ce5a Merge pull request #123 from qhy040404/windowhook 2025-01-17 17:13:59 +08:00
qhy040404
957c8b98e4 add window hook entry 2025-01-17 17:11:25 +08:00
REL
01a3f41323 [skip ci] fix 2025-01-11 19:51:13 +08:00
HolographicHat
32ceae074e [skip ci] export player store data 2025-01-11 19:24:42 +08:00
HolographicHat
2e2be07161 update docs 2025-01-10 16:23:36 +08:00
HolographicHat
832c82f44e Merge pull request #122 from 34736384/master 2025-01-10 14:42:02 +08:00
REL
43b38df986 fix bugs 2025-01-09 01:05:46 -05:00
REL
6e1c8f275f refactor 2025-01-07 23:56:16 -05:00
REL
e65f046520 added PlayerStoreNotify 2025-01-07 23:36:27 -05:00
HolographicHat
4025729677 ci 2025-01-08 09:32:51 +08:00
HolographicHat
07050c1c3d move sources 2025-01-08 09:30:17 +08:00
REL
e56a6228aa use cpp23 and 'modernize' 2025-01-07 20:16:14 -05:00
HolographicHat
66b29b1374 static linking 2024-11-30 14:13:51 +08:00
HolographicHat
247c401a5b aot 2024-11-30 13:52:39 +08:00
HolographicHat
cf9d601b27 rin 2024-11-30 11:56:41 +08:00
HolographicHat
5abb9e2934 Merge pull request #118 from Mikachu2333/master 2024-11-22 13:07:50 +08:00
HolographicHat
1b861712eb fix #119 #120 2024-11-21 21:35:41 +08:00
LinkChou
d62377ad96 prepare for next version 2024-11-21 10:31:53 +08:00
HolographicHat
fb4e3f8d00 fix 2024-11-21 08:11:26 +08:00
HolographicHat
b1135542c1 5.2 2024-11-21 07:33:07 +08:00
HolographicHat
7d3d0f5e14 auto identify field ids 2024-11-21 07:31:21 +08:00
HolographicHat
4268b04f3c ignore zydis 2024-10-15 20:41:04 +08:00
HolographicHat
9a9d1310a1 Merge pull request #115 from 34736384/master 2024-10-15 20:37:59 +08:00
REL
618a9189ad use sse 2024-10-15 07:32:14 -04:00
REL
f73dbdc4fe switch to amalgamated zydis 2024-10-14 20:24:32 -04:00
REL
298134c063 added dynamic resolving of CmdId and ToUInt16 2024-10-14 05:54:32 -04:00
REL
e9ace26d69 refactor 2024-10-10 05:06:57 -04:00
REL
2c15353f86 remove detours 2024-10-09 08:32:36 -04:00
REL
99fec63867 remove il2cpp bloat 2024-10-09 08:14:41 -04:00
HolographicHat
bf5525d2ea 5.1 2024-10-09 16:03:25 +08:00
HolographicHat
cf3749f887 fix #114 2024-09-12 23:47:01 +08:00
HolographicHat
21af4de1a6 5.0 2024-08-29 18:24:21 +08:00
HolographicHat
8e0fd2d27c fix #111 2024-08-05 18:11:51 +08:00
HolographicHat
0348cfa365 Merge pull request #107 from eltociear/add-japanese-readme-tutorial 2024-07-31 07:58:19 +08:00
Ikko Eltociear Ashimine
494eda32c2 docs: add Japanese README and tutorial 2024-07-23 00:47:25 +09:00
HolographicHat
975638c1ee Merge pull request #104 from canmengxian/master 2024-07-17 18:33:19 +08:00
HolographicHat
793ad075fe 4.8 2024-07-17 18:12:24 +08:00
残梦
c82a10353f Update Utils.cs 2024-06-23 23:16:13 +08:00
HolographicHat
f737122247 4.7 2024-06-06 02:07:56 +08:00
HolographicHat
520167ef85 fix #82 #101 2024-04-29 14:38:13 +08:00
HolographicHat
faee6f6121 Update README_EN.md 2024-04-25 21:50:15 +08:00
HolographicHat
06c5468118 update readme 2024-04-25 21:42:14 +08:00
HolographicHat
b7c2204f68 Update CI 2024-04-25 02:52:46 +08:00
HolographicHat
5dc5e646d6 fix #87 2024-04-25 02:45:30 +08:00
74 changed files with 70222 additions and 2772 deletions

2
.gitattributes vendored
View File

@@ -1,4 +1,4 @@
lib/src/il2cpp-types.h linguist-generated=true lib/src/Zydis.* linguist-generated=true
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto

View File

@@ -12,11 +12,11 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
@@ -24,7 +24,7 @@ jobs:
- name: Publish - name: Publish
run: dotnet publish --property:OutputPath=.\publish\ run: dotnet publish --property:OutputPath=.\publish\
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v4
with: with:
name: Artifacts name: Artifacts
path: publish path: YaeAchievement\publish\publish

30
.github/workflows/lib-nuget.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: YaeLib NuGet Publish
on:
workflow_dispatch:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Restore NuGet Packages
run: nuget restore lib\YaeAchievementLib.sln
- name: Build
continue-on-error: true
run: msbuild lib\YaeAchievementLib.sln /p:Configuration=Release
- name: Pack
run: nuget pack lib\YaeAchievementLib.nuspec
- name: Publish to NuGet
run: nuget push *.nupkg ${{ secrets.NUGET_API_KEY }} -src https://api.nuget.org/v3/index.json

19
.gitignore vendored
View File

@@ -1,15 +1,6 @@
bin/ bin
obj/ obj
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea .idea
.vs
# User-specific files publish
*.suo sync
*.user
*.userosscache
*.sln.docstates
.vs/
src/Proto/*

View File

@@ -1,42 +1,42 @@
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/icon.ico"> <div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
# YaeAchievement # YaeAchievement
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) ![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
简体中文 | [English](README_EN.md) 简体中文 | [English](README_EN.md) | [日本語](README_JP.md)
</div> </div>
- 支持导出所有类别的成就 - 支持导出所有类别的成就
- 支持官服,渠道服与国际服 - 支持官服,渠道服与国际服
- 没有窗口大小、游戏语言等要求 - 没有窗口大小、游戏语言等要求
## 导出支持 ## 导出支持
> 按照数字键选择导出方式,<kbd>0</kbd> 为默认导出方式 > 按照数字键选择导出方式,<kbd>0</kbd> 为默认导出方式
0. [椰羊](https://cocogoat.work/achievement) 0. [椰羊](https://cocogoat.work/achievement)
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao) 1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/) 2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements) 3. [Seelie.me](https://seelie.me/achievements)
4. 表格文件 `.csv` 4. 表格文件 `.csv`
5. [寻空](https://github.com/xunkong/xunkong) 5. [寻空](https://github.com/xunkong/xunkong)
6. [原魔工具箱](https://apps.apple.com/app/id1663989619) 6. [原魔工具箱](https://apps.apple.com/app/id1663989619)
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide) 7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON 文件 8. [UIAF](https://uigf.org/standards/UIAF.html) JSON 文件
## 使用说明 ## 使用说明
→ [Tutorial.md](Tutorial.md) → [Tutorial.md](Tutorial.md)
## 下载地址 ## 下载地址
[releases/latest](https://github.com/HolographicHat/YaeAchievement/releases/latest) [releases/latest](https://github.com/HolographicHat/YaeAchievement/releases/latest)
## 问题反馈 ## 问题反馈
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi) [issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 598720036](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## 常见问题 ## 常见问题
0. Q: 打不开 0. Q: 打不开
A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-7.0.3-windows-x64-installer) A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer)
1. Q: 原神启动时报错: 数据异常(31-4302) 1. Q: 原神启动时报错: 数据异常(31-4302)
A: 不要把软件和原神主程序放一起 A: 不要把软件和原神主程序放一起

View File

@@ -1,13 +1,10 @@
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/icon.ico"> <div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
# YaeAchievement # YaeAchievement
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) ![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
[简体中文](README.md) | English [简体中文](README.md) | English | [日本語](README_JP.md)
**I18n support currently in [snapshot version](https://github.com/HolographicHat/YaeAchievement/actions/)**
**Next release: 2022/09/28 (Genshin 3.1)**
</div> </div>
@@ -35,11 +32,11 @@
## Download: [Here](https://github.com/HolographicHat/YaeAchievement/releases/latest) ## Download: [Here](https://github.com/HolographicHat/YaeAchievement/releases/latest)
## Feedback or Problem? ## Feedback or Problem?
[issues](https://github.com/HolographicHat/YaeAchievement/issues) or [QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi) [issues](https://github.com/HolographicHat/YaeAchievement/issues) or [QQ群: 598720036](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## Frequently asked questions ## Frequently asked questions
0. Q: Unable to start 0. Q: Unable to start
A: Download and install [.NET Runtime 7](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-7.0.7-windows-x64-installer) or ` winget install Microsoft.DotNet.Runtime.7` A: Download and install [.NET Runtime 9](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer) or ` winget install Microsoft.DotNet.Runtime.9`
1. Q: Error while Genshin started: Data Exception (31-4302) 1. Q: Error while Genshin started: Data Exception (31-4302)

44
README_JP.md Normal file
View File

@@ -0,0 +1,44 @@
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
# YaeAchievement
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
[简体中文](README.md) | [English](README_EN.md) | 日本語
</div>
- すべてのカテゴリの実績のエクスポートをサポート
- すべてのバージョンの原神をサポート
- ウィンドウサイズ、ゲーム言語などの要件はありません
## エクスポートサポート
> 数字キーに従ってエクスポート方法を選択します。<kbd>0</kbd> はデフォルトのエクスポート方法です
0. [椰羊](https://cocogoat.work/achievement)
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements)
4. フォームファイル `.csv`
5. [尋空](https://github.com/xunkong/xunkong)
6. [原魔ツールボックス](https://apps.apple.com/app/id1663989619)
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSONファイル
## 使用説明書:
→ [Tutorial_JP.md](Tutorial_JP.md)
## ダウンロード: [こちら](https://github.com/HolographicHat/YaeAchievement/releases/latest)
## フィードバックや問題?
[issues](https://github.com/HolographicHat/YaeAchievement/issues) または [QQ群: 598720036](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## よくある質問
0. Q: 起動できない
A: [.NET Runtime 9](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer) をダウンロードしてインストールするか、`winget install Microsoft.DotNet.Runtime.9` を実行してください。
1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302)
A: ソフトウェアを原神のディレクトリに配置しないでください。

View File

@@ -6,17 +6,17 @@
点击图中红框圈中的名称为“YaeAchievement.exe”的文件浏览器会自动弹出下载。建议将该文件保存在桌面或者其它易于寻找的文件夹内。 点击图中红框圈中的名称为“YaeAchievement.exe”的文件浏览器会自动弹出下载。建议将该文件保存在桌面或者其它易于寻找的文件夹内。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/1.png) ![1](https://github.com/user-attachments/assets/8b98c018-b179-4681-992d-367a0f522dae)
2.安装启动软件所需文件(若已安装该运行时可忽略此步骤) 2.安装启动软件所需文件(若已安装该运行时可忽略此步骤)
点击该网址https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-7.0.3-windows-x64-installer 。 点击该网址https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer 。
进入网页后浏览器会自动弹出下载,同样地,将文件保存在桌面或者其它易于寻找的文件夹内。 进入网页后浏览器会自动弹出下载,同样地,将文件保存在桌面或者其它易于寻找的文件夹内。
下载完成后打开名称形如dotnet-runtime-x.x.x-win-x64.exe的文件会弹出安装窗口如下图所示。 下载完成后打开名称形如dotnet-runtime-x.x.x-win-x64.exe的文件会弹出安装窗口如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/2.png) ![2](https://github.com/user-attachments/assets/4830a824-34c0-479e-9c9c-fc23e99003bf)
直接点击安装即可。 直接点击安装即可。
@@ -24,13 +24,13 @@
双击在第一步下载的名称为“YaeAchievement.exe”的文件成功打开后会提示原神正在启动如下图所示。 双击在第一步下载的名称为“YaeAchievement.exe”的文件成功打开后会提示原神正在启动如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/3.png) ![3](https://github.com/user-attachments/assets/3d9eb78b-187f-4ada-90b7-5a951c9d0ee1)
原神启动完成后,点击进入游戏即可。 原神启动完成后,点击进入游戏即可。
点击进入游戏后原神闪退,工具会提示您选择导出至何种工具,如下图所示。 点击进入游戏后原神自动退出,工具会提示您选择导出至何种工具,如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/4.png) ![4](https://github.com/user-attachments/assets/76b64d42-2865-4f61-9ceb-8e317af50e7e)
此时可根据自己的需要进行选择,一般推荐导出至[0]椰羊以及[4]表格文件(.csv 此时可根据自己的需要进行选择,一般推荐导出至[0]椰羊以及[4]表格文件(.csv
@@ -38,35 +38,35 @@
#### 椰羊: #### 椰羊:
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/5.png) ![5](https://github.com/user-attachments/assets/9e3188ab-cfad-4cfc-8db9-51ae22ff7caa)
#### Snap.Hutao #### Snap.Hutao
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main//images/6.png) ![6](https://github.com/user-attachments/assets/8d8cabe1-1ebc-4329-898b-0c725a5b10e4)
#### Seelie.me #### Seelie.me
此时YaeAchievement会提示成就数据已导出。请在保存YaeAchievement的文件夹内找到名称形如export-20xxxxxxxxxxxx-seelie.json的文件。 此时YaeAchievement会提示成就数据已导出。请在保存YaeAchievement的文件夹内找到名称形如export-20xxxxxxxxxxxx-seelie.json的文件。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/7.png) ![7](https://github.com/user-attachments/assets/c0c6724e-90cd-4e58-b441-a3af97493455)
然后点击该网址https://seelie.me/settings, 进入网页后选择导入,如下图所示。 然后点击该网址https://seelie.me/settings, 进入网页后选择导入,如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/8.png) ![8](https://github.com/user-attachments/assets/4182f3dd-723d-452b-aa32-2246ad710ac1)
点击导入后选中名称形如export-20xxxxxxxxxxxx-seelie.json的文件如下图所示。 点击导入后选中名称形如export-20xxxxxxxxxxxx-seelie.json的文件如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/9.png) ![9](https://github.com/user-attachments/assets/9466ee0b-a5e9-4bf5-9f95-5ac86ea0c8f6)
当弹出如下图所示的提示时表示导入成功。 当弹出如下图所示的提示时表示导入成功。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/10.png) ![A](https://github.com/user-attachments/assets/755193d6-41f6-4108-b9ca-867e92107a03)
此时可选择左栏成就,查看导入的成就数据。 此时可选择左栏成就,查看导入的成就数据。
#### 寻空: #### 寻空:
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/11.png) ![B](https://github.com/user-attachments/assets/f78c9a70-0b81-4c19-a034-5ed9f8e6eff4)
### 各种工具的介绍烦请移步至各工具的官方页面进行查看(下方序号对应导出序号) ### 各种工具的介绍烦请移步至各工具的官方页面进行查看(下方序号对应导出序号)
@@ -74,7 +74,7 @@
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao) 1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/) 2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements) 3. [Seelie.me](https://seelie.me/achievements)
4. ~~表格文件 `.csv`~~ 4. [表格文件 `.csv`](https://en.wikipedia.org/wiki/Comma-separated_values)
5. [寻空](https://github.com/xunkong/xunkong) 5. [寻空](https://github.com/xunkong/xunkong)
6. [原魔工具箱](https://apps.apple.com/app/id1663989619) 6. [原魔工具箱](https://apps.apple.com/app/id1663989619)
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide) 7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)

View File

@@ -1,25 +1,25 @@
## Instructions for Use # Instructions for Use
1.Download YaeAchievementLatest Version 1.Download YaeAchievementLatest Version
Click Herehttps://github.com/HolographicHat/YaeAchievement/releases Click Here<https://github.com/HolographicHat/YaeAchievement/releases>
Click on the file named "YaeAchievement.exe" in the red box to automatically pop up and download.It is recommended that you save this file in a desktop or other easy-to-see folder. Click on the file named "YaeAchievement.exe" in the red box to automatically pop up and download.It is recommended that
you save this file in a desktop or other easy-to-see folder.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide1.png) ![Step1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3)
2.Install .NET Runtime 7 (this step can be ignored if the runtime is already installed) 2.Install .NET Runtime 9 (this step can be ignored if the runtime is already installed)
Click Herehttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-7.0.7-windows-x64-installer . Click Herehttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer .
Or `winget install Microsoft.DotNet.Runtime.7` if you use Windows 11 or have Winget installed. Or `winget install Microsoft.DotNet.Runtime.9` if you use Windows 11 or have Winget installed.
The browser automatically pops up and downloads when you enter the web page, as well as saving files in a desktop or other easy-to-see folder. The browser automatically pops up and downloads when you enter the web page, as well as saving files in a desktop or other easy-to-see folder.
When you open a file with the name dotnet-runtime-x.x.x-win-x64.exe after the download is complete, an installation window pops up, as shown below. When you open a file with the name dotnet-runtime-x.x.x-win-x64.exe after the download is complete, an installation window pops up, as shown below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide2.png) ![Guide2](https://github.com/user-attachments/assets/35f421af-dd45-41ea-94f9-e3cf90710f0f)
Just click Install. Just click Install.
@@ -27,13 +27,13 @@ Just click Install.
Double-click the file named "YaeAchievement.exe" downloaded in the first step to open it successfully, indicating that the original god is starting, as shown below. Double-click the file named "YaeAchievement.exe" downloaded in the first step to open it successfully, indicating that the original god is starting, as shown below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide3.png) ![Guide3](https://github.com/user-attachments/assets/c3375188-1fa3-4a0b-9007-358afbcaae91)
Once the primordial startup is complete, click to enter the game. Once the primordial startup is complete, click to enter the game.
When you click into the game, the tool prompts you to choose which tool to export, as shown below. When you click into the game, the tool prompts you to choose which tool to export, as shown below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide4.png) ![Guide4](https://github.com/user-attachments/assets/c806582a-3608-4c86-af26-ce8e631ff610)
For global user, you should select [3] Seelie.me or [4] Export to csv file。 For global user, you should select [3] Seelie.me or [4] Export to csv file。
@@ -41,25 +41,25 @@ After selecting, each page exports the tool as follows:
#### Snap.Hutao #### Snap.Hutao
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide5.png) ![Guide5](https://github.com/user-attachments/assets/40d547d8-fe04-4462-8b78-284394a44c36)
#### Seelie.me #### Seelie.me
At this point, Yae Achievement will remind that performance data has been exported. Please find the file named export-20xxxxxxxxxxxx-seelie.json in the Yae Achievement save directory. At this point, Yae Achievement will remind that performance data has been exported. Please find the file named export-20xxxxxxxxxxxx-seelie.json in the Yae Achievement save directory.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide6.png) ![Guide6](https://github.com/user-attachments/assets/91cdb0e6-883d-4f5e-9f1f-416eb8c16433)
Then click on the URL: https://seelie.me/settings, enter the website and select Import Account, as illustrated in the figure below. Then click on the URL: https://seelie.me/settings, enter the website and select Import Account, as illustrated in the figure below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide7.png) ![Guide7](https://github.com/user-attachments/assets/e6a9ddb1-b075-4f0b-9e42-a1d61b4808bc)
After clicking Import, select a file named export-20xxxxxxxxxxxx-seelie.json, as shown in the figure below. After clicking Import, select a file named export-20xxxxxxxxxxxx-seelie.json, as shown in the figure below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide8.png) ![Guide8](https://github.com/user-attachments/assets/1b7edb51-ff0d-415c-bd96-0e6c9ac7a238)
When the prompt as shown in the image below pops up, the import process succeeds. When the prompt as shown in the image below pops up, the import process succeeds.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide9.png) ![Guide9](https://github.com/user-attachments/assets/e155b4e5-ce15-4dd8-9633-a83b9759bce1)
At this time, you can select the Achievements in the left column to view the imported performance data. At this time, you can select the Achievements in the left column to view the imported performance data.
@@ -69,7 +69,7 @@ At this time, you can select the Achievements in the left column to view the imp
1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao) 1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/) 2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements) 3. [Seelie.me](https://seelie.me/achievements)
4. ~~Form File `.csv`~~ 4. [Form File `.csv`](https://en.wikipedia.org/wiki/Comma-separated_values)
5. [XunKong](https://github.com/xunkong/xunkong) 5. [XunKong](https://github.com/xunkong/xunkong)
6. [YuanmoTools](https://apps.apple.com/app/id1663989619) 6. [YuanmoTools](https://apps.apple.com/app/id1663989619)
7. [Teyvat Guide](https://github.com/BTMuli/TeyvatGuide) 7. [Teyvat Guide](https://github.com/BTMuli/TeyvatGuide)

75
Tutorial_JP.md Normal file
View File

@@ -0,0 +1,75 @@
## 使用説明書
1. YaeAchievement最新バージョンをダウンロード
こちらをクリックhttps://github.com/HolographicHat/YaeAchievement/releases
赤枠で囲まれた「YaeAchievement.exe」という名前のファイルをクリックすると、自動的にポップアップしてダウンロードされます。このファイルをデスクトップや他の見やすいフォルダに保存することをお勧めします。
![Guide1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3)
2. .NET Runtime 9をインストールランタイムが既にインストールされている場合はこのステップを無視できます
こちらをクリックhttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer .
または、Windows 11を使用しているか、Wingetがインストールされている場合は、`winget install Microsoft.DotNet.Runtime.9`を実行します。
ウェブページにアクセスすると、ブラウザが自動的にポップアップしてダウンロードされます。ファイルをデスクトップや他の見やすいフォルダに保存します。
ダウンロードが完了したら、dotnet-runtime-x.x.x-win-x64.exeという名前のファイルを開くと、インストールウィンドウがポップアップします。以下の図のように表示されます。
![Guide2](https://github.com/user-attachments/assets/35f421af-dd45-41ea-94f9-e3cf90710f0f)
インストールをクリックするだけです。
3. メインプログラムを開くための操作と実績エクスポートのオプション
最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。
![Guide3](https://github.com/user-attachments/assets/c3375188-1fa3-4a0b-9007-358afbcaae91)
原神の起動が完了したら、ゲームに入ります。
ゲームに入ると、ツールがどのツールにエクスポートするかを選択するように促します。以下の図のように表示されます。
![Guide4](https://github.com/user-attachments/assets/c806582a-3608-4c86-af26-ce8e631ff610)
グローバルユーザーの場合、[3] Seelie.meまたは[4] csvファイルにエクスポートを選択する必要があります。
選択後、各ページは次のようにツールをエクスポートします:
#### Snap.Hutao
![Guide5](https://github.com/user-attachments/assets/40d547d8-fe04-4462-8b78-284394a44c36)
#### Seelie.me
この時点で、Yae Achievementは実績データがエクスポートされたことを通知します。Yae Achievement保存ディレクトリにexport-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを見つけてください。
![Guide6](https://github.com/user-attachments/assets/91cdb0e6-883d-4f5e-9f1f-416eb8c16433)
次に、URLhttps://seelie.me/settings をクリックし、ウェブサイトにアクセスしてインポートアカウントを選択します。以下の図のように表示されます。
![Guide7](https://github.com/user-attachments/assets/e6a9ddb1-b075-4f0b-9e42-a1d61b4808bc)
インポートをクリックした後、export-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを選択します。以下の図のように表示されます。
![Guide8](https://github.com/user-attachments/assets/1b7edb51-ff0d-415c-bd96-0e6c9ac7a238)
以下の図のようなプロンプトが表示されたら、インポートプロセスは成功です。
![Guide9](https://github.com/user-attachments/assets/e155b4e5-ce15-4dd8-9633-a83b9759bce1)
この時点で、左側の列のAchievementsを選択して、インポートされた実績データを表示できます。
### 各ツールの紹介については、各ツールの公式ページをご覧ください:
0. [椰羊](https://cocogoat.work/achievement)
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements)
4. [フォームファイル `.csv`](https://ja.wikipedia.org/wiki/Comma-separated_values)
5. [尋空](https://github.com/xunkong/xunkong)
6. [原魔ツールボックス](https://apps.apple.com/app/id1663989619)
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSONファイル

View File

@@ -1,61 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationManifest>res\app.manifest</ApplicationManifest>
<AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>2.0.0</FileVersion>
<ApplicationIcon>icon.ico</ApplicationIcon>
<DebugType>embedded</DebugType>
<SelfContained>false</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.26.1" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Grpc.Tools" Version="2.62.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="res\App.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>App.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Remove="res\Updater.exe" />
<None Remove="src\NativeMethods.json" />
<None Remove="src\NativeMethods.txt" />
<AdditionalFiles Include="src\NativeMethods.json" />
<AdditionalFiles Include="src\NativeMethods.txt" />
<EmbeddedResource Include="res\Updater.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="res\App.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>App.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None" />
</ItemGroup>
</Project>

View File

@@ -1,16 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeAchievement", "YaeAchievement.csproj", "{FA5FA5E1-3B38-4AF9-8CBA-A7E9193D660C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FA5FA5E1-3B38-4AF9-8CBA-A7E9193D660C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA5FA5E1-3B38-4AF9-8CBA-A7E9193D660C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA5FA5E1-3B38-4AF9-8CBA-A7E9193D660C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA5FA5E1-3B38-4AF9-8CBA-A7E9193D660C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

3
YaeAchievement.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#" />
</Solution>

View File

@@ -2,5 +2,5 @@
"$schema": "https://aka.ms/CsWin32.schema.json", "$schema": "https://aka.ms/CsWin32.schema.json",
"className": "Native", "className": "Native",
"allowMarshaling": false, "allowMarshaling": false,
"public": true "public": true
} }

View File

@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFramework>net9.0-windows</TargetFramework>
<FileVersion>5.3.0</FileVersion>
<AssemblyVersion>5.3.0</AssemblyVersion>
<ApplicationIcon>res\icon.ico</ApplicationIcon>
<ApplicationManifest>res\app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Google.Protobuf" Version="3.30.2" />
<PackageReference Include="Grpc.Tools" Version="2.71.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Spectre.Console" Version="0.50.1-preview.0.3" />
<PackageReference Include="Spectre.Console.Analyzer" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="res\App.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>App.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Remove="res\updater.exe"/>
<EmbeddedResource Include="res\updater.exe" LogicalName="updater"/>
</ItemGroup>
<ItemGroup>
<Compile Update="res\App.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>App.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None"/>
</ItemGroup>
<PropertyGroup>
<CETCompat>false</CETCompat>
<!-- <TrimmerSingleWarn>false</TrimmerSingleWarn>-->
</PropertyGroup>
</Project>

View File

@@ -86,6 +86,24 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized string similar to No.
/// </summary>
internal static string CommonNo {
get {
return ResourceManager.GetString("CommonNo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Yes.
/// </summary>
internal static string CommonYes {
get {
return ResourceManager.GetString("CommonYes", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to You need to login genshin impact before exporting.. /// Looks up a localized string similar to You need to login genshin impact before exporting..
/// </summary> /// </summary>
@@ -114,16 +132,7 @@ namespace YaeAchievement.res {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Export to: /// Looks up a localized string similar to Export to:.
///[0] Cocogoat (https://cocogoat.work/achievement, Default)
///[1] Snap.HuTao
///[2] Paimon.moe
///[3] Seelie.me
///[4] Csv file
///[5] Xunkong
///[7] Teyvat Guide
///[8] UIAF JSON File
///Input a number (0-8): .
/// </summary> /// </summary>
internal static string ExportChoose { internal static string ExportChoose {
get { get {
@@ -131,6 +140,87 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized string similar to Cocogoat (https://cocogoat.work/achievement).
/// </summary>
internal static string ExportTargetCocogoat {
get {
return ResourceManager.GetString("ExportTargetCocogoat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Csv file.
/// </summary>
internal static string ExportTargetCsv {
get {
return ResourceManager.GetString("ExportTargetCsv", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Snap.HuTao.
/// </summary>
internal static string ExportTargetHuTao {
get {
return ResourceManager.GetString("ExportTargetHuTao", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Paimon.moe.
/// </summary>
internal static string ExportTargetPaimon {
get {
return ResourceManager.GetString("ExportTargetPaimon", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Seelie.me.
/// </summary>
internal static string ExportTargetSeelie {
get {
return ResourceManager.GetString("ExportTargetSeelie", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Teyvat Guide.
/// </summary>
internal static string ExportTargetTeyvatGuide {
get {
return ResourceManager.GetString("ExportTargetTeyvatGuide", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to UIAF JSON File.
/// </summary>
internal static string ExportTargetUIAFJson {
get {
return ResourceManager.GetString("ExportTargetUIAFJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string ExportTargetWxApp1 {
get {
return ResourceManager.GetString("ExportTargetWxApp1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Xunkong.
/// </summary>
internal static string ExportTargetXunkong {
get {
return ResourceManager.GetString("ExportTargetXunkong", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Fail, please contact developer to get help information. /// Looks up a localized string similar to Fail, please contact developer to get help information.
/// </summary> /// </summary>
@@ -320,6 +410,15 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized string similar to Checking update....
/// </summary>
internal static string UpdateChecking {
get {
return ResourceManager.GetString("UpdateChecking", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Description: /// Looks up a localized string similar to Description:
///{0}. ///{0}.
@@ -348,16 +447,6 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Updater {
get {
object obj = ResourceManager.GetObject("Updater", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Upload error to appcenter.... /// Looks up a localized string similar to Upload error to appcenter....
/// </summary> /// </summary>
@@ -368,7 +457,7 @@ namespace YaeAchievement.res {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Use previous fetched data? (yes|no). /// Looks up a localized string similar to Use previous fetched data?.
/// </summary> /// </summary>
internal static string UsePreviousData { internal static string UsePreviousData {
get { get {
@@ -393,5 +482,14 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("VcRuntimeInstalling", resourceCulture); return ResourceManager.GetString("VcRuntimeInstalling", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Please update game and retry..
/// </summary>
internal static string WaitMetadataUpdate {
get {
return ResourceManager.GetString("WaitMetadataUpdate", resourceCulture);
}
}
} }
} }

View File

@@ -3,7 +3,7 @@
<root> <root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
</xsd:element> </xsd:element>
</xsd:schema> </xsd:schema>
<resheader name="resmimetype"> <resheader name="resmimetype">
@@ -25,16 +25,7 @@
<value>all achievement</value> <value>all achievement</value>
</data> </data>
<data name="ExportChoose" xml:space="preserve"> <data name="ExportChoose" xml:space="preserve">
<value>Export to: <value>Export to:</value>
[0] Cocogoat (https://cocogoat.work/achievement, Default)
[1] Snap.HuTao
[2] Paimon.moe
[3] Seelie.me
[4] Csv file
[5] Xunkong
[7] Teyvat Guide
[8] UIAF JSON File
Input a number (0-8): </value>
</data> </data>
<data name="ExportToCocogoatSuccess" xml:space="preserve"> <data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>Successfully exported to cocogoat.</value> <value>Successfully exported to cocogoat.</value>
@@ -107,7 +98,7 @@ Input a number (0-8): </value>
<value>YaeAchievement ({0})</value> <value>YaeAchievement ({0})</value>
</data> </data>
<data name="UsePreviousData" xml:space="preserve"> <data name="UsePreviousData" xml:space="preserve">
<value>Use previous fetched data? (yes|no)</value> <value>Use previous fetched data?</value>
</data> </data>
<data name="NetworkError" xml:space="preserve"> <data name="NetworkError" xml:space="preserve">
<value>Network error:</value> <value>Network error:</value>
@@ -121,10 +112,6 @@ Input a number (0-8): </value>
<data name="ExceptionNetwork" xml:space="preserve"> <data name="ExceptionNetwork" xml:space="preserve">
<value>Network error ({0}: {1})</value> <value>Network error ({0}: {1})</value>
</data> </data>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="Updater" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Updater.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="GenshinHashError" xml:space="preserve"> <data name="GenshinHashError" xml:space="preserve">
<value>Please update genshin and retry.</value> <value>Please update genshin and retry.</value>
</data> </data>
@@ -137,4 +124,43 @@ Input a number (0-8): </value>
<data name="ExportToTauriFail" xml:space="preserve"> <data name="ExportToTauriFail" xml:space="preserve">
<value>Please launch/update Teyvat Guide and retry.</value> <value>Please launch/update Teyvat Guide and retry.</value>
</data> </data>
<data name="WaitMetadataUpdate" xml:space="preserve">
<value>Please update game and retry.</value>
</data>
<data name="UpdateChecking" xml:space="preserve">
<value>Checking update...</value>
</data>
<data name="CommonYes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="CommonNo" xml:space="preserve">
<value>No</value>
</data>
<data name="ExportTargetCocogoat" xml:space="preserve">
<value>Cocogoat (https://cocogoat.work/achievement)</value>
</data>
<data name="ExportTargetHuTao" xml:space="preserve">
<value>Snap.HuTao</value>
</data>
<data name="ExportTargetPaimon" xml:space="preserve">
<value>Paimon.moe</value>
</data>
<data name="ExportTargetSeelie" xml:space="preserve">
<value>Seelie.me</value>
</data>
<data name="ExportTargetCsv" xml:space="preserve">
<value>Csv file</value>
</data>
<data name="ExportTargetXunkong" xml:space="preserve">
<value>Xunkong</value>
</data>
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
<value>Teyvat Guide</value>
</data>
<data name="ExportTargetUIAFJson" xml:space="preserve">
<value>UIAF JSON File</value>
</data>
<data name="ExportTargetWxApp1" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -18,17 +18,7 @@
<value>全部成就</value> <value>全部成就</value>
</data> </data>
<data name="ExportChoose" xml:space="preserve"> <data name="ExportChoose" xml:space="preserve">
<value>导出至: <value>导出到哪里?(键盘上下键移动光标,键盘回车键选择)</value>
[0] 椰羊 (https://cocogoat.work/achievement, 默认)
[1] Snap Hutao
[2] Paimon.moe
[3] Seelie.me
[4] 表格文件
[5] 寻空
[6] 原魔工具箱
[7] Teyvat Guide
[8] UIAF JSON 文件
输入一个数字 (0-8): </value>
</data> </data>
<data name="ExportToCocogoatSuccess" xml:space="preserve"> <data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>在浏览器内进行下一步操作</value> <value>在浏览器内进行下一步操作</value>
@@ -101,7 +91,7 @@
<value>YaeAchievement - 原神成就导出工具 ({0})</value> <value>YaeAchievement - 原神成就导出工具 ({0})</value>
</data> </data>
<data name="UsePreviousData" xml:space="preserve"> <data name="UsePreviousData" xml:space="preserve">
<value>要使用上一次获取到的成就数据吗? (yes|no)</value> <value>要使用上一次获取到的成就数据吗?(键盘上下键移动光标,键盘回车键选择)</value>
</data> </data>
<data name="NetworkError" xml:space="preserve"> <data name="NetworkError" xml:space="preserve">
<value>网络错误: {0}</value> <value>网络错误: {0}</value>
@@ -127,4 +117,43 @@
<data name="ExportToTauriSuccess" xml:space="preserve"> <data name="ExportToTauriSuccess" xml:space="preserve">
<value>在 Teyvat Guide 进行下一步操作</value> <value>在 Teyvat Guide 进行下一步操作</value>
</data> </data>
<data name="WaitMetadataUpdate" xml:space="preserve">
<value>当前元数据版本不匹配,请更新原神至最新版本或等待元数据更新后重试。</value>
</data>
<data name="UpdateChecking" xml:space="preserve">
<value>正在检查更新...</value>
</data>
<data name="CommonYes" xml:space="preserve">
<value>是</value>
</data>
<data name="CommonNo" xml:space="preserve">
<value>否</value>
</data>
<data name="ExportTargetCocogoat" xml:space="preserve">
<value>椰羊 (https://cocogoat.work/achievement)</value>
</data>
<data name="ExportTargetHuTao" xml:space="preserve">
<value>Snap Hutao</value>
</data>
<data name="ExportTargetPaimon" xml:space="preserve">
<value>Paimon.moe</value>
</data>
<data name="ExportTargetSeelie" xml:space="preserve">
<value>Seelie.me</value>
</data>
<data name="ExportTargetCsv" xml:space="preserve">
<value>表格文件</value>
</data>
<data name="ExportTargetXunkong" xml:space="preserve">
<value>寻空</value>
</data>
<data name="ExportTargetUIAFJson" xml:space="preserve">
<value>UIAF JSON 文件</value>
</data>
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
<value>Teyvat Guide</value>
</data>
<data name="ExportTargetWxApp1" xml:space="preserve">
<value>原魔工具箱</value>
</data>
</root> </root>

Binary file not shown.

View File

@@ -49,7 +49,7 @@
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI无需 自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI无需
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应 选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。 在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation --> 将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <application xmlns="urn:schemas-microsoft-com:asm.v3">

View File

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -2,6 +2,14 @@ syntax = "proto3";
option csharp_namespace = "Proto"; option csharp_namespace = "Proto";
message AchievementProtoFieldInfo {
uint32 id = 1;
uint32 status = 2;
uint32 total_progress = 3;
uint32 current_progress = 4;
uint32 finish_timestamp = 5;
}
message AchievementItem { message AchievementItem {
uint32 pre = 1; uint32 pre = 1;
uint32 group = 2; uint32 group = 2;
@@ -13,4 +21,5 @@ message AchievementInfo {
string version = 1; string version = 1;
map<uint32, string> group = 2; map<uint32, string> group = 2;
map<uint32, AchievementItem> items = 3; map<uint32, AchievementItem> items = 3;
AchievementProtoFieldInfo pb_info = 4;
} }

View File

@@ -0,0 +1,79 @@
syntax = "proto3";
option csharp_namespace = "Proto";
enum StoreType {
STORE_TYPE_NONE = 0;
STORE_TYPE_PACK = 1;
STORE_TYPE_DEPOT = 2;
}
message MaterialDeleteInfo {
message CountDownDelete {
map<uint32, uint32> delete_time_num_map = 1;
uint32 config_count_down_time = 2;
}
message DateTimeDelete {
uint32 delete_time = 1;
}
message DelayWeekCountDownDelete {
map<uint32, uint32> delete_time_num_map = 1;
uint32 config_delay_week = 2;
uint32 config_count_down_time = 3;
}
bool has_delete_config = 1;
oneof delete_info {
CountDownDelete count_down_delete = 2;
DateTimeDelete date_delete = 3;
DelayWeekCountDownDelete delay_week_count_down_delete = 4;
}
}
message Material {
uint32 count = 1;
MaterialDeleteInfo delete_info = 2;
}
message Reliquary {
uint32 level = 1;
uint32 exp = 2;
uint32 promote_level = 3;
uint32 main_prop_id = 4;
repeated uint32 append_prop_id_list = 5;
bool is_marked = 6;
}
message Weapon {
uint32 level = 1;
uint32 exp = 2;
uint32 promote_level = 3;
map<uint32, uint32> affix_map = 4;
bool is_arkhe_ousia = 5;
}
message Equip {
oneof detail {
Reliquary reliquary = 1;
Weapon weapon = 2;
}
bool is_locked = 3;
}
message Furniture {
uint32 count = 1;
}
message VirtualItem {
int64 count = 1;
}
message Item {
uint32 item_id = 1;
uint64 guid = 2;
oneof detail {
Material material = 5;
Equip equip = 6;
Furniture furniture = 7;
VirtualItem virtual_item = 255;
}
}

View File

@@ -0,0 +1,15 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message UpdateInfo {
uint32 version_code = 1;
string version_name = 2;
string description = 3;
string package_link = 4;
bool force_update = 5;
bool enable_lib_download = 6;
bool enable_auto_update = 7;
string current_cn_hash = 8;
string current_os_hash = 9;
}

View File

@@ -0,0 +1,54 @@
using System.Text.RegularExpressions;
using YaeAchievement.res;
using YaeAchievement.Utilities;
namespace YaeAchievement;
public static partial class AppConfig {
public static string GamePath { get; private set; } = null!;
private static readonly string[] ProductNames = [ "原神", "Genshin Impact" ];
internal static void Load(string argumentPath) {
if (argumentPath != "auto" && File.Exists(argumentPath)) {
GamePath = argumentPath;
return;
}
if (CacheFile.TryRead("genshin_impact_game_path", out var cache)) {
var path = cache.Content.ToStringUtf8();
if (path != null && File.Exists(path)) {
GamePath = path;
return;
}
}
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var logPath = ProductNames
.Select(name => $"{appDataPath}/../LocalLow/miHoYo/{name}/output_log.txt")
.Where(File.Exists)
.MaxBy(File.GetLastWriteTime);
if (logPath == null) {
throw new ApplicationException(App.ConfigNeedStartGenshin);
}
GamePath = GetGamePathFromLogFile(logPath)
?? GetGamePathFromLogFile($"{logPath}.last")
?? throw new ApplicationException(App.ConfigNeedStartGenshin);
}
private static string? GetGamePathFromLogFile(string path) {
if (!File.Exists(path)) {
return null;
}
var content = File.ReadAllText(path);
var matchResult = GamePathRegex().Match(content);
if (!matchResult.Success) {
return null;
}
var entryName = matchResult.Groups["1"].Value.Replace("_Data", ".exe");
return Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
}
[GeneratedRegex(@"(?m).:(?:\\|/).+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
private static partial Regex GamePathRegex();
}

View File

@@ -0,0 +1,238 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Win32;
using Spectre.Console;
using YaeAchievement.Outputs;
using YaeAchievement.Parsers;
using YaeAchievement.res;
// ReSharper disable UnusedMember.Local
namespace YaeAchievement;
public static class Export {
public static int ExportTo { get; set; } = 114514;
public static void Choose(AchievementAllDataNotify data) {
var targets = new Dictionary<string, Action<AchievementAllDataNotify>> {
{ App.ExportTargetCocogoat, ToCocogoat },
{ App.ExportTargetHuTao, ToHuTao },
{ App.ExportTargetPaimon, ToPaimon },
{ App.ExportTargetSeelie, ToSeelie },
{ App.ExportTargetCsv, ToCSV },
{ App.ExportTargetXunkong, ToXunkong },
// { App.ExportTargetWxApp1, ToWxApp1 },
{ App.ExportTargetTeyvatGuide, ToTeyvatGuide },
{ App.ExportTargetUIAFJson, ToUIAFJson },
// { "", ToRawJson }
};
Action<AchievementAllDataNotify> action;
if (ExportTo == 114514) {
var prompt = new SelectionPrompt<string>().Title(App.ExportChoose).AddChoices(targets.Keys);
action = targets[AnsiConsole.Prompt(prompt)];
} else {
action = targets.ElementAtOrDefault(ExportTo).Value ?? ToCocogoat;
}
action(data);
}
private static void ToCocogoat(AchievementAllDataNotify data) {
var result = UIAFSerializer.Serialize(data);
using var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
request.RequestUri = new Uri($"https://77.cocogoat.cn/v1/memo?source={App.AllAchievement}");
request.Content = new StringContent(result, Encoding.UTF8, "application/json");
using var response = Utils.CHttpClient.Send(request);
if (response.StatusCode != HttpStatusCode.Created) {
AnsiConsole.WriteLine(App.ExportToCocogoatFail);
return;
}
var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var responseJson = JsonSerializer.Deserialize(responseText, CocogoatResponseContext.Default.CocogoatResponse)!;
var cocogoatUrl = $"https://cocogoat.work/achievement?memo={responseJson.Key}";
Utils.SetQuickEditMode(true);
AnsiConsole.MarkupLineInterpolated($"[link]{cocogoatUrl}[/]");
if (Utils.ShellOpen(cocogoatUrl))
{
AnsiConsole.WriteLine(App.ExportToCocogoatSuccess);
}
}
private static void ToWxApp1(AchievementAllDataNotify data) {
var id = Guid.NewGuid().ToString("N").Substring(20, 8);
var result = WxApp1Serializer.Serialize(data, id);
using var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
request.RequestUri = new Uri("https://api.qyinter.com/achievementRedis");
request.Content = new StringContent(result, Encoding.UTF8, "application/json");
using var response = Utils.CHttpClient.Send(request);
AnsiConsole.WriteLine(App.ExportToWxApp1Success, id);
}
private static void ToHuTao(AchievementAllDataNotify data) {
if (CheckWinUIAppScheme("hutao")) {
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
Utils.ShellOpen("hutao://achievement/import");
AnsiConsole.WriteLine(App.ExportToSnapGenshinSuccess);
} else {
AnsiConsole.WriteLine(App.ExportToSnapGenshinNeedUpdate);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9PH4NXJ2JN52");
}
}
private static void ToXunkong(AchievementAllDataNotify data) {
if (CheckWinUIAppScheme("xunkong")) {
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
Utils.ShellOpen("xunkong://import-achievement?caller=YaeAchievement&from=clipboard");
AnsiConsole.WriteLine(App.ExportToXunkongSuccess);
} else {
AnsiConsole.WriteLine(App.ExportToXunkongNeedUpdate);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9N2SVG0JMT12");
}
}
private static void ToTeyvatGuide(AchievementAllDataNotify data) {
if (Process.GetProcessesByName("TeyvatGuide").Length != 0) {
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
Utils.ShellOpen("teyvatguide://import_uiaf?app=Yae");
AnsiConsole.WriteLine(App.ExportToTauriSuccess);
} else {
AnsiConsole.WriteLine(App.ExportToTauriFail);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9NLBNNNBNSJN");
}
}
// ReSharper disable once InconsistentNaming
private static void ToUIAFJson(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"uiaf-{DateTime.Now:yyyyMMddHHmmss}.json");
if (TryWriteToFile(path, UIAFSerializer.Serialize(data))) {
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
}
}
private static void ToPaimon(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-paimon.json");
if (TryWriteToFile(path, PaimonSerializer.Serialize(data))) {
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
}
}
private static void ToSeelie(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-seelie.json");
if (TryWriteToFile(path, SeelieSerializer.Serialize(data))) {
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
}
}
// ReSharper disable once InconsistentNaming
private static void ToCSV(AchievementAllDataNotify data) {
var info = GlobalVars.AchievementInfo;
var outList = new List<List<object>>();
foreach (var ach in data.AchievementList.OrderBy(a => a.Id)) {
if (UnusedAchievement.Contains(ach.Id)) continue;
if (!info.Items.TryGetValue(ach.Id, out var achInfo) || achInfo == null) {
AnsiConsole.WriteLine($@"Unable to find {ach.Id} in metadata.");
continue;
}
var finishAt = "";
if (ach.FinishTimestamp != 0) {
var ts = Convert.ToInt64(ach.FinishTimestamp);
finishAt = DateTimeOffset.FromUnixTimeSeconds(ts).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
}
var current = ach.Status != AchievementStatus.Unfinished
? ach.CurrentProgress == 0 ? ach.TotalProgress : ach.CurrentProgress
: ach.CurrentProgress;
outList.Add([
ach.Id, ach.Status.ToDesc(), achInfo.Group, achInfo.Name,
achInfo.Description, current, ach.TotalProgress, finishAt
]);
}
var output = new List<string> { "ID,状态,特辑,名称,描述,当前进度,目标进度,完成时间" };
output.AddRange(outList.OrderBy(v => v[2]).Select(item => {
item[2] = info.Group[(uint) item[2]];
return item.JoinToString(",");
}));
var path = Path.GetFullPath($"achievement-{DateTime.Now:yyyyMMddHHmmss}.csv");
if (TryWriteToFile(path, $"\uFEFF{string.Join("\n", output)}")) {
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
Process.Start("explorer.exe", $"{Path.GetDirectoryName(path)}");
}
}
private static void ToRawJson(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-raw.json");
var text = AchievementRawDataSerializer.Serialize(data);
if (TryWriteToFile(path, text)) {
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
}
}
// ReSharper disable once InconsistentNaming
private static bool CheckWinUIAppScheme(string protocol) {
return (string?)Registry.ClassesRoot.OpenSubKey(protocol)?.GetValue("") == $"URL:{protocol}";
}
private static string JoinToString(this IEnumerable<object> list, string separator) {
return string.Join(separator, list);
}
private static readonly List<uint> UnusedAchievement = [ 84517 ];
private static string ToDesc(this AchievementStatus status) {
return status switch {
AchievementStatus.Invalid => App.StatusInvalid,
AchievementStatus.Finished => App.StatusFinished,
AchievementStatus.Unfinished => App.StatusUnfinished,
AchievementStatus.RewardTaken => App.StatusRewardTaken,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
};
}
public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) {
// ReSharper disable once LocalizableElement
AnsiConsole.WriteLine($"{msg}: {ex.Message}");
return ex.NativeErrorCode;
}
private static bool TryWriteToFile(string path, string contents) {
try {
File.WriteAllText(path, contents);
return true;
} catch (UnauthorizedAccessException) {
AnsiConsole.WriteLine(App.NoWritePermission, path);
return false;
}
}
}
public class WxApp1Root {
public string Key { get; init; } = null!;
public UIAFRoot Data { get; init; } = null!;
}
[JsonSerializable(typeof(WxApp1Root))]
[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)]
public partial class WxApp1Serializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf, string key) => JsonSerializer.Serialize(new WxApp1Root {
Key = key,
Data = Outputs.UIAFRoot.FromNotify(ntf)
}, Default.WxApp1Root);
}
public record CocogoatResponse(string Key);
[JsonSerializable(typeof(CocogoatResponse))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class CocogoatResponseContext : JsonSerializerContext;

View File

@@ -1,33 +1,32 @@
using System.Reflection; using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Proto;
namespace YaeAchievement; namespace YaeAchievement;
// ReSharper disable InconsistentNaming
// ReSharper disable ConvertToConstant.Global
// ReSharper disable FieldCanBeMadeReadOnly.Global
// ReSharper disable once MemberCanBePrivate.Global
public static class GlobalVars { public static class GlobalVars {
public static bool DebugProxy => false;
public static bool UnexpectedExit { get; set; } = true;
public static bool PauseOnExit { get; set; } = true; public static bool PauseOnExit { get; set; } = true;
public static Version AppVersion { get; } = Assembly.GetEntryAssembly()!.GetName().Version!; public static Version AppVersion { get; } = Assembly.GetEntryAssembly()!.GetName().Version!;
public static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory; public static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory;
private static readonly string CommonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); private static readonly string CommonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
public static readonly string DataPath = Path.Combine(CommonData, "Yae"); public static readonly string DataPath = Path.Combine(CommonData, "Yae");
public static readonly string CachePath = Path.Combine(DataPath, "cache"); public static readonly string CachePath = Path.Combine(DataPath, "cache");
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll"); public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
public const uint AppVersionCode = 46; public const uint AppVersionCode = 236;
public const string AppVersionName = "3.6"; public const string AppVersionName = "5.4";
public const string PipeName = "YaeAchievementPipe"; public const string PipeName = "YaeAchievementPipe";
public const string BucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
[field:MaybeNull]
public static AchievementInfo AchievementInfo =>
field ??= AchievementInfo.Parser.ParseFrom(Utils.GetBucketFile("schicksal/metadata").GetAwaiter().GetResult());
static GlobalVars() { static GlobalVars() {
Directory.CreateDirectory(DataPath); Directory.CreateDirectory(DataPath);
Directory.CreateDirectory(CachePath); Directory.CreateDirectory(CachePath);
} }
} }

View File

@@ -0,0 +1,37 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using YaeAchievement.Parsers;
namespace YaeAchievement.Outputs;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable PropertyCanBeMadeInitOnly.Global
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global
public class PaimonRoot {
public Dictionary<uint, Dictionary<uint, bool>> Achievement { get; set; } = null!;
public static PaimonRoot FromNotify(AchievementAllDataNotify ntf) {
var info = GlobalVars.AchievementInfo.Items.ToDictionary(pair => pair.Key, pair => pair.Value.Group);
return new PaimonRoot {
Achievement = ntf.AchievementList
.Where(a => a.Status >= AchievementStatus.Finished && info.ContainsKey(a.Id))
.GroupBy(a => info[a.Id], a => a.Id)
.OrderBy(g => g.Key)
.ToDictionary(g => g.Key, g => g.ToDictionary(id => id, _ => true))
};
}
}
[JsonSerializable(typeof(PaimonRoot))]
[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)]
public partial class PaimonSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(Outputs.PaimonRoot.FromNotify(ntf), Default.PaimonRoot);
}
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using YaeAchievement.Parsers;
namespace YaeAchievement.Outputs;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable PropertyCanBeMadeInitOnly.Global
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global
public class SeelieRoot {
public class AchievementFinishStatus {
public bool Done => true;
}
public Dictionary<uint, AchievementFinishStatus> Achievements { get; set; } = null!;
public static SeelieRoot FromNotify(AchievementAllDataNotify ntf) => new () {
Achievements = ntf.AchievementList
.Where(a => a.Status >= AchievementStatus.Finished)
.OrderBy(a => a.Id)
.ToDictionary(a => a.Id, _ => new AchievementFinishStatus())
};
}
[JsonSerializable(typeof(SeelieRoot))]
[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)]
public partial class SeelieSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(Outputs.SeelieRoot.FromNotify(ntf), Default.SeelieRoot);
}
}

View File

@@ -0,0 +1,65 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using YaeAchievement.Parsers;
namespace YaeAchievement.Outputs;
// ReSharper disable InconsistentNaming
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable PropertyCanBeMadeInitOnly.Global
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global
public class UApplicationInfo {
public string ExportApp => "YaeAchievement";
public string ExportAppVersion => GlobalVars.AppVersionName;
public long ExportTimestamp => DateTimeOffset.Now.ToUnixTimeSeconds();
public string UIAFVersion => "v1.1";
}
public class UAchievementInfo {
public uint Id { get; set; }
public uint Status { get; set; }
public uint Current { get; set; }
public uint Timestamp { get; set; }
}
public class UIAFRoot {
public UApplicationInfo Info => new ();
public IEnumerable<UAchievementInfo> List { get; set; } = null!;
public static UIAFRoot FromNotify(AchievementAllDataNotify ntf) => new () {
List = ntf.AchievementList
.Where(a => a.Status >= AchievementStatus.Finished || a.CurrentProgress > 0)
.Select(a => new UAchievementInfo {
Id = a.Id,
Status = (uint) a.Status,
Current = a.CurrentProgress,
Timestamp = a.FinishTimestamp
})
};
}
[JsonSerializable(typeof(UIAFRoot))]
[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)]
public partial class UIAFSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(Outputs.UIAFRoot.FromNotify(ntf), Default.UIAFRoot);
}
}

View File

@@ -0,0 +1,140 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Spectre.Console;
using YaeAchievement.res;
using YaeAchievement.Utilities;
namespace YaeAchievement.Parsers;
public enum AchievementStatus {
Invalid,
Unfinished,
Finished,
RewardTaken,
}
public class AchievementItem {
public uint Id { get; init; }
public uint TotalProgress { get; init; }
public uint CurrentProgress { get; init; }
public uint FinishTimestamp { get; init; }
public AchievementStatus Status { get; init; }
}
public class AchievementAllDataNotify {
public List<AchievementItem> AchievementList { get; private init; } = [];
private static AchievementAllDataNotify? Instance { get; set; }
public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32());
CacheFile.Write("achievement_data", bytes);
Instance = ParseFrom(bytes);
return true;
}
public static void OnFinish() {
if (Instance == null) {
throw new ApplicationException("No data received");
}
Export.Choose(Instance);
}
public static AchievementAllDataNotify ParseFrom(byte[] bytes) {
using var stream = new CodedInputStream(bytes);
var data = new List<Dictionary<uint, uint>>();
var errTimes = 0;
try {
uint tag;
while ((tag = stream.ReadTag()) != 0) {
if ((tag & 7) == 2) { // is LengthDelimited
var dict = new Dictionary<uint, uint>();
using var eStream = stream.ReadLengthDelimitedAsStream();
try {
while ((tag = eStream.ReadTag()) != 0) {
if ((tag & 7) != 0) { // not VarInt
dict = null;
break;
}
dict[tag >> 3] = eStream.ReadUInt32();
}
if (dict != null) {
data.Add(dict);
}
} catch (InvalidProtocolBufferException) {
if (errTimes++ > 0) { // allows 1 fail on 'reward_taken_goal_id_list'
throw;
}
}
}
}
} catch (InvalidProtocolBufferException) {
// ReSharper disable once LocalizableElement
AnsiConsole.WriteLine("Parse failed");
File.WriteAllBytes("achievement_raw_data.bin", bytes);
Environment.Exit(0);
}
if (data.Count == 0) {
return new AchievementAllDataNotify();
}
uint tId, sId, iId, currentId, totalId;
if (data.Count > 20) { /* uwu */
(tId, var cnt) = data // ↓ 2020-09-15 04:15:14
.GroupKeys(value => value > 1600114514).Select(g => (g.Key, g.Count())).MaxBy(p => p.Item2);
sId = data // FINISHED ↓ ↓ REWARD_TAKEN
.GroupKeys(value => value is 2 or 3).First(g => g.Count() == cnt).Key;
iId = data // ↓ id: 8xxxx
.GroupKeys(value => value / 10000 % 10 == 8).MaxBy(g => g.Count())!.Key;
(currentId, totalId) = data
.Where(d => d[sId] is 2 or 3)
.Select(d => d.ToDictionary().RemoveValues(tId, sId, iId).ToArray())
.Where(d => d.Length == 2 && d[0].Value != d[1].Value)
.GroupBy(a => a[0].Value > a[1].Value ? (a[0].Key, a[1].Key) : (a[1].Key, a[0].Key))
.Select(g => (FieldIds: g.Key, Count: g.Count()))
.MaxBy(p => p.Count)
.FieldIds;
#if DEBUG
// ReSharper disable once LocalizableElement
AnsiConsole.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}");
#endif
} else {
var info = GlobalVars.AchievementInfo.PbInfo; // ...
iId = info.Id;
tId = info.FinishTimestamp;
sId = info.Status;
totalId = info.TotalProgress;
currentId = info.CurrentProgress;
if (data.Any(dict => !dict.ContainsKey(iId) || !dict.ContainsKey(sId) || !dict.ContainsKey(totalId))) {
AnsiConsole.WriteLine(App.WaitMetadataUpdate);
Environment.Exit(0);
}
}
return new AchievementAllDataNotify {
AchievementList = data.Select(dict => new AchievementItem {
Id = dict[iId],
Status = (AchievementStatus) dict[sId],
TotalProgress = dict[totalId],
CurrentProgress = dict.GetValueOrDefault(currentId),
FinishTimestamp = dict.GetValueOrDefault(tId),
}).ToList()
};
}
}
[JsonSerializable(typeof(AchievementAllDataNotify))]
[JsonSourceGenerationOptions(
WriteIndented = true,
GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)]
public partial class AchievementRawDataSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(ntf, Default.AchievementAllDataNotify);
}
}

View File

@@ -0,0 +1,106 @@
using Proto;
using static YaeAchievement.Parsers.PropType;
namespace YaeAchievement.Parsers;
public enum PropType {
None = 0,
Exp = 1001,
BreakLevel = 1002,
SatiationVal = 1003,
SatiationPenaltyTime = 1004,
GearStartVal = 2001,
GearStopVal = 2002,
Level = 4001,
LastChangeAvatarTime = 10001,
MaxSpringVolume = 10002,
CurSpringVolume = 10003,
IsSpringAutoUse = 10004,
SpringAutoUsePercent = 10005,
IsFlyable = 10006,
IsWeatherLocked = 10007,
IsGameTimeLocked = 10008,
IsTransferable = 10009,
MaxStamina = 10010,
CurPersistStamina = 10011,
CurTemporaryStamina = 10012,
PlayerLevel = 10013,
PlayerExp = 10014,
PlayerHCoin = 10015,
PlayerSCoin = 10016,
PlayerMpSettingType = 10017,
IsMpModeAvailable = 10018,
PlayerWorldLevel = 10019,
PlayerResin = 10020,
PlayerWaitSubHCoin = 10022,
PlayerWaitSubSCoin = 10023,
IsOnlyMpWithPsPlayer = 10024,
PlayerMCoin = 10025,
PlayerWaitSubMCoin = 10026,
PlayerLegendaryKey = 10027,
IsHasFirstShare = 10028,
PlayerForgePoint = 10029,
CurClimateMeter = 10035,
CurClimateType = 10036,
CurClimateAreaId = 10037,
CurClimateAreaClimateType = 10038,
PlayerWorldLevelLimit = 10039,
PlayerWorldLevelAdjustCd = 10040,
PlayerLegendaryDailyTaskNum = 10041,
PlayerHomeCoin = 10042,
PlayerWaitSubHomeCoin = 10043,
IsAutoUnlockSpecificEquip = 10044,
PlayerGCGCoin = 10045,
PlayerWaitSubGCGCoin = 10046,
PlayerOnlineTime = 10047,
IsDiveable = 10048,
MaxDiveStamina = 10049,
CurPersistDiveStamina = 10050,
IsCanPutFiveStarReliquary = 10051,
IsAutoLockFiveStarReliquary = 10052,
PlayerRoleCombatCoin = 10053,
CurPhlogiston = 10054,
ReliquaryTemporaryExp = 10055,
IsMpCrossPlatformEnabled = 10056,
IsOnlyMpWithPlatformPlayer = 10057,
PlayerMusicGameBookCoin = 10058,
IsNotShowReliquaryRecommendProp = 10059,
}
public static class PlayerPropNotify {
private static readonly Dictionary<PropType, double> PropMap = [];
public static bool OnReceive(BinaryReader reader) {
var propType = (PropType) reader.ReadInt32();
var propValue = reader.ReadDouble();
PropMap.Add(propType, propValue);
return false;
}
public static void OnFinish() {
PlayerStoreNotify.Instance.ItemList.AddRange([
CreateVirtualItem(201, GetPropValue(PlayerHCoin) - GetPropValue(PlayerWaitSubHCoin)),
CreateVirtualItem(202, GetPropValue(PlayerSCoin) - GetPropValue(PlayerWaitSubSCoin)),
CreateVirtualItem(203, GetPropValue(PlayerMCoin) - GetPropValue(PlayerWaitSubMCoin)),
CreateVirtualItem(204, GetPropValue(PlayerHomeCoin) - GetPropValue(PlayerWaitSubHomeCoin)),
CreateVirtualItem(206, GetPropValue(PlayerRoleCombatCoin)),
CreateVirtualItem(207, GetPropValue(PlayerMusicGameBookCoin)),
]);
}
private static Item CreateVirtualItem(uint id, double count) {
return new Item {
ItemId = id,
VirtualItem = new VirtualItem {
Count = (long) count
}
};
}
private static double GetPropValue(PropType propType) {
return PropMap.GetValueOrDefault(propType);
}
}

View File

@@ -0,0 +1,61 @@
using Google.Protobuf;
using Proto;
using Spectre.Console;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable CollectionNeverQueried.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
namespace YaeAchievement.Parsers;
public class PlayerStoreNotify {
public uint WeightLimit { get; set; }
public StoreType StoreType { get; set; }
public List<Item> ItemList { get; set; } = [];
public static PlayerStoreNotify Instance { get; } = new ();
public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32());
Instance.ParseFrom(bytes);
return true;
}
private void ParseFrom(byte[] bytes) {
using var stream = new CodedInputStream(bytes);
try {
uint tag;
while ((tag = stream.ReadTag()) != 0) {
var wireType = tag & 7;
switch (wireType) {
case 0: { // is VarInt
var value = stream.ReadUInt32();
if (value < 10) {
StoreType = (StoreType) value;
} else {
WeightLimit = value;
}
continue;
}
case 2: { // is LengthDelimited
using var eStream = stream.ReadLengthDelimitedAsStream();
while (eStream.PeekTag() != 0) {
ItemList.Add(Item.Parser.ParseFrom(eStream));
}
break;
}
}
}
} catch (InvalidProtocolBufferException) {
// ReSharper disable once LocalizableElement
AnsiConsole.WriteLine("Parse failed");
File.WriteAllBytes("store_raw_data.bin", bytes);
Environment.Exit(0);
}
}
}

View File

@@ -0,0 +1,78 @@
using System.Runtime.CompilerServices;
using System.Text;
using Spectre.Console;
using YaeAchievement.Parsers;
using YaeAchievement.res;
using YaeAchievement.Utilities;
using static YaeAchievement.Utils;
namespace YaeAchievement;
// TODO: WndHook
internal static class Program {
public static async Task Main(string[] args) {
AnsiConsole.WriteLine(@"----------------------------------------------------");
AnsiConsole.WriteLine(App.AppBanner, GlobalVars.AppVersionName);
AnsiConsole.WriteLine(@"https://github.com/HolographicHat/YaeAchievement");
AnsiConsole.WriteLine(@"----------------------------------------------------");
if (!new Mutex(true, @"Global\YaeMiku~uwu").WaitOne(0, false)) {
AnsiConsole.WriteLine(App.AnotherInstance);
Environment.Exit(302);
}
InstallExitHook();
InstallExceptionHook();
CheckGenshinIsRunning();
AppConfig.Load(args.GetOrNull(0) ?? "auto");
Export.ExportTo = ToIntOrDefault(args.GetOrNull(1), 114514);
await CheckUpdate(ToBooleanOrDefault(args.GetOrNull(2)));
AchievementAllDataNotify? data = null;
try {
if (CacheFile.TryRead("achievement_data", out var cache)) {
data = AchievementAllDataNotify.ParseFrom(cache.Content.ToByteArray());
}
} catch (Exception) { /* ignored */ }
if (CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow && data != null) {
var prompt = new SelectionPrompt<string>()
.Title(App.UsePreviousData)
.AddChoices(App.CommonYes, App.CommonNo);
if (AnsiConsole.Prompt(prompt) == App.CommonYes) {
Export.Choose(data);
return;
}
}
StartAndWaitResult(AppConfig.GamePath, new Dictionary<int, Func<BinaryReader, bool>> {
{ 1, AchievementAllDataNotify.OnReceive },
{ 2, PlayerStoreNotify.OnReceive },
{ 100, PlayerPropNotify.OnReceive },
}, () => {
#if DEBUG_EX
PlayerPropNotify.OnFinish();
File.WriteAllText("store_data.json", JsonSerializer.Serialize(PlayerStoreNotify.Instance, new JsonSerializerOptions {
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
}));
#endif
AchievementAllDataNotify.OnFinish();
Environment.Exit(0);
});
while (true) {}
}
[ModuleInitializer]
internal static void SetupConsole() {
SetQuickEditMode(false);
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;
}
}

View File

@@ -0,0 +1,53 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using Google.Protobuf;
using Proto;
namespace YaeAchievement.Utilities;
public static class CacheFile {
static CacheFile() {
// remove deprecated cache
foreach (var file in Directory.EnumerateFiles(GlobalVars.CachePath, "*.miko")) {
File.Delete(file);
}
}
public static DateTime GetLastWriteTime(string id) {
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
return File.Exists(fileName) ? File.GetLastWriteTimeUtc(fileName) : DateTime.UnixEpoch;
}
public static bool TryRead(string id, [NotNullWhen(true)] out CacheItem? item) {
item = null;
try {
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
using var fileStream = File.OpenRead(fileName);
using var zipStream = new GZipStream(fileStream, CompressionMode.Decompress);
item = CacheItem.Parser.ParseFrom(zipStream);
return true;
} catch (Exception) {
return false;
}
}
public static void Write(string id, byte[] data, string? etag = null) {
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
using var fileStream = File.Open(fileName, FileMode.Create);
using var zipStream = new GZipStream(fileStream, CompressionLevel.SmallestSize);
new CacheItem {
Etag = etag ?? string.Empty,
Version = 3,
Checksum = GetBinHash(data),
Content = ByteString.CopyFrom(data)
}.WriteTo(zipStream);
}
private static string GetStrHash(string value) => GetBinHash(Encoding.UTF8.GetBytes(value));
private static string GetBinHash(byte[] value) => Convert.ToHexStringLower(MD5.HashData(value));
}

View File

@@ -0,0 +1,16 @@
// ReSharper disable once CheckNamespace
namespace System.Collections.Generic;
public static class Collection {
public static IDictionary<TKey, TValue> RemoveValues<TKey, TValue>(
this IDictionary<TKey, TValue> dictionary,
params TKey[] keys
) {
foreach (var key in keys) {
dictionary.Remove(key);
}
return dictionary;
}
}

View File

@@ -0,0 +1,14 @@
// ReSharper disable once CheckNamespace
namespace System.Linq;
public static class Enumerable {
public static IEnumerable<IGrouping<TKey, TKey>> GroupKeys<TKey, TValue>(
this IEnumerable<Dictionary<TKey, TValue>> source,
Func<TValue, bool> condition
) where TKey : notnull {
return source.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key)).GroupBy(x => x);
}
}

View File

@@ -0,0 +1,16 @@
using System.Runtime.CompilerServices;
// ReSharper disable CheckNamespace
namespace Google.Protobuf;
public static class CodedInputStreamExtensions {
[UnsafeAccessor(UnsafeAccessorKind.Method)]
private static extern byte[] ReadRawBytes(CodedInputStream stream, int size);
public static CodedInputStream ReadLengthDelimitedAsStream(this CodedInputStream stream) {
return new CodedInputStream(ReadRawBytes(stream, stream.ReadLength()));
}
}

View File

@@ -0,0 +1,73 @@
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Threading;
using static Windows.Win32.System.Memory.VIRTUAL_ALLOCATION_TYPE;
using static Windows.Win32.System.Memory.PAGE_PROTECTION_FLAGS;
using static Windows.Win32.System.Memory.VIRTUAL_FREE_TYPE;
// ReSharper disable MemberCanBePrivate.Global
namespace YaeAchievement.Utilities;
public unsafe class GameProcess {
public uint Id { get; }
public HANDLE Handle { get; }
public HANDLE MainThreadHandle { get; }
public event Action? OnExit;
public GameProcess(string path) {
const PROCESS_CREATION_FLAGS flags = PROCESS_CREATION_FLAGS.CREATE_SUSPENDED;
Span<char> cmdLines = stackalloc char[1]; // "\0"
var si = new STARTUPINFOW {
cb = (uint) sizeof(STARTUPINFOW)
};
var wd = Path.GetDirectoryName(path)!;
if (!Native.CreateProcess(path, ref cmdLines, null, null, false, flags, null, wd, si, out var pi)) {
throw new ApplicationException($"CreateProcess fail: {Marshal.GetLastPInvokeErrorMessage()}");
}
Id = pi.dwProcessId;
Handle = pi.hProcess;
MainThreadHandle = pi.hThread;
Task.Run(() => {
Native.WaitForSingleObject(Handle, 0xFFFFFFFF); // INFINITE
OnExit?.Invoke();
});
}
public void LoadLibrary(string libPath) {
var hKrnl32 = NativeLibrary.Load("kernel32");
var mLoadLibraryW = NativeLibrary.GetExport(hKrnl32, "LoadLibraryW");
var libPathLen = (uint) libPath.Length * sizeof(char);
var lpLibPath = Native.VirtualAllocEx(Handle, default, libPathLen + 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (lpLibPath == null) {
throw new ApplicationException($"VirtualAllocEx fail: {Marshal.GetLastPInvokeErrorMessage()}");
}
fixed (void* lpBuffer = libPath) {
if (!Native.WriteProcessMemory(Handle, lpLibPath, lpBuffer, libPathLen)) {
throw new ApplicationException($"WriteProcessMemory fail: {Marshal.GetLastPInvokeErrorMessage()}");
}
}
var lpStartAddress = (delegate*unmanaged[Stdcall]<void*, uint>) mLoadLibraryW; // THREAD_START_ROUTINE
var hThread = Native.CreateRemoteThread(Handle, default, 0, lpStartAddress, lpLibPath, 0);
if (hThread.IsNull) {
var error = Marshal.GetLastPInvokeErrorMessage();
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
throw new ApplicationException($"CreateRemoteThread fail: {error}");
}
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
}
Native.CloseHandle(hThread);
}
public bool ResumeMainThread() => Native.ResumeThread(MainThreadHandle) != 0xFFFFFFFF;
public bool Terminate(uint exitCode) => Native.TerminateProcess(Handle, exitCode);
}

241
YaeAchievement/src/Utils.cs Normal file
View File

@@ -0,0 +1,241 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using Proto;
using Spectre.Console;
using YaeAchievement.res;
using YaeAchievement.Utilities;
namespace YaeAchievement;
public static class Utils {
public static HttpClient CHttpClient { get; } = new (new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
}) {
DefaultRequestHeaders = {
UserAgent = {
new ProductInfoHeaderValue("YaeAchievement", GlobalVars.AppVersion.ToString(2))
}
}
};
public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
try {
return await GetFile("https://api.qhy04.com/hutaocdn/download?filename={0}", path, useCache);
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
}
try {
return await Task.WhenAny(
GetFile("https://rin.holohat.work/{0}", path, useCache),
GetFile("https://cn-cd-1259389942.file.myqcloud.com/{0}", path, useCache)
).Unwrap();
} catch (Exception ex) when (ex is SocketException or TaskCanceledException) {
AnsiConsole.WriteLine(App.NetworkError, ex.Message);
Environment.Exit(-1);
}
return null!;
static async Task<byte[]> GetFile(string baseUrl, string objectKey, bool useCache) {
using var reqwest = new HttpRequestMessage(HttpMethod.Get, string.Format(baseUrl, objectKey));
CacheItem? cache = null;
if (useCache && CacheFile.TryRead(objectKey, out cache)) {
reqwest.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
}
using var response = await CHttpClient.SendAsync(reqwest);
if (cache != null && response.StatusCode == HttpStatusCode.NotModified) {
return cache.Content.ToByteArray();
}
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
if (useCache) {
var etag = response.Headers.ETag!.Tag;
CacheFile.Write(objectKey, bytes, etag);
}
return bytes;
}
}
public static T? GetOrNull<T>(this T[] array, uint index) where T : class {
return array.Length > index ? array[index] : null;
}
public static int ToIntOrDefault(string? value, int defaultValue = 0) {
return value != null && int.TryParse(value, out var result) ? result : defaultValue;
}
public static bool ToBooleanOrDefault(string? value, bool defaultValue = false) {
return value != null && bool.TryParse(value, out var result) ? result : defaultValue;
}
public static unsafe void CopyToClipboard(string text) {
if (Native.OpenClipboard(HWND.Null)) {
Native.EmptyClipboard();
var hGlobal = (HGLOBAL) Marshal.AllocHGlobal((text.Length + 1) * 2);
var hPtr = (nint) Native.GlobalLock(hGlobal);
Marshal.Copy(text.ToCharArray(), 0, hPtr, text.Length);
Native.GlobalUnlock((HGLOBAL) hPtr);
Native.SetClipboardData(13, new HANDLE(hPtr));
Marshal.FreeHGlobal(hGlobal);
Native.CloseClipboard();
} else {
throw new Win32Exception();
}
}
// ReSharper disable once NotAccessedField.Local
private static UpdateInfo _updateInfo = null!;
public static Task StartSpinnerAsync(string status, Func<StatusContext, Task> func) {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
}
public static Task<T> StartSpinnerAsync<T>(string status, Func<StatusContext, Task<T>> func) {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
}
public static async Task CheckUpdate(bool useLocalLib) {
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
var versionInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
if (versionInfo.EnableAutoUpdate) {
var newBin = await StartSpinnerAsync(App.UpdateDownloading, _ => GetBucketFile(versionInfo.PackageLink));
var tmpPath = Path.GetTempFileName();
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
await using (var dstStream = File.Open($"{GlobalVars.DataPath}/update.exe", FileMode.Create)) {
await using var srcStream = typeof(Program).Assembly.GetManifestResourceStream("updater")!;
await srcStream.CopyToAsync(dstStream);
}
await File.WriteAllBytesAsync(tmpPath, newBin);
ShellOpen(updaterPath, $"{Environment.ProcessId} \"{tmpPath}\"");
await StartSpinnerAsync(App.UpdateChecking, _ => Task.Delay(1919810));
GlobalVars.PauseOnExit = false;
Environment.Exit(0);
}
AnsiConsole.MarkupLine($"[link]{App.DownloadLink}[/]", versionInfo.PackageLink);
if (versionInfo.ForceUpdate) {
Environment.Exit(0);
}
}
if (versionInfo.EnableLibDownload && !useLocalLib) {
var data = await GetBucketFile("schicksal/lic.dll");
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data);
}
_updateInfo = versionInfo;
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static bool ShellOpen(string path, string? args = null) {
try {
var startInfo = new ProcessStartInfo {
FileName = path,
UseShellExecute = true
};
if (args != null) {
startInfo.Arguments = args;
}
return new Process {
StartInfo = startInfo
}.Start();
} catch (Exception) {
return false;
}
}
internal static void CheckGenshinIsRunning() {
// QueryProcessEvent?
var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
foreach (var path in Directory.EnumerateDirectories($"{appdata}/../LocalLow/miHoYo").Where(p => File.Exists($"{p}/info.txt"))) {
try {
using var handle = File.OpenHandle($"{path}/output_log.txt", share: FileShare.None);
} catch (IOException) {
AnsiConsole.WriteLine(App.GenshinIsRunning, 0);
Environment.Exit(301);
}
}
}
private static GameProcess? _proc;
public static void InstallExitHook() {
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
_proc?.Terminate(0);
if (GlobalVars.PauseOnExit) {
AnsiConsole.WriteLine(App.PressKeyToExit);
Console.ReadKey();
}
};
}
public static void InstallExceptionHook() {
AppDomain.CurrentDomain.UnhandledException += (_, e) => {
var ex = e.ExceptionObject;
switch (ex) {
case ApplicationException ex1:
AnsiConsole.WriteLine(ex1.Message);
break;
case SocketException ex2:
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
break;
case HttpRequestException ex3:
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
break;
default:
AnsiConsole.WriteLine(ex.ToString()!);
break;
}
Environment.Exit(-1);
};
}
private static bool _isUnexpectedExit = true;
// ReSharper disable once UnusedMethodReturnValue.Global
public static void StartAndWaitResult(string exePath, Dictionary<int, Func<BinaryReader, bool>> handlers, Action onFinish) {
_proc = new GameProcess(exePath);
_proc.OnExit += () => {
if (_isUnexpectedExit) {
_proc = null;
AnsiConsole.WriteLine(App.GameProcessExit);
Environment.Exit(114514);
}
};
_proc.LoadLibrary(GlobalVars.LibFilePath);
_proc.ResumeMainThread();
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
Task.Run(() => {
using var stream = new NamedPipeServerStream(GlobalVars.PipeName);
using var reader = new BinaryReader(stream);
stream.WaitForConnection();
int type;
while ((type = stream.ReadByte()) != -1) {
if (type == 0xFF) {
_isUnexpectedExit = false;
onFinish();
break;
}
if (handlers.TryGetValue(type, out var handler)) {
if (handler(reader)) {
handlers.Remove(type);
}
}
}
});
}
public static unsafe void SetQuickEditMode(bool enable) {
var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
CONSOLE_MODE mode = default;
Native.GetConsoleMode(handle, &mode);
mode = enable ? mode | CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE : mode &~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE;
Native.SetConsoleMode(handle, mode);
}
}

View File

@@ -0,0 +1,17 @@
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>Yae.Lib</id>
<version>5.3.3</version>
<authors>HolographicHat</authors>
<developmentDependency>true</developmentDependency>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">GPL-3.0-only</license>
<licenseUrl>https://licenses.nuget.org/GPL-3.0-only</licenseUrl>
<projectUrl>https://github.com/HolographicHat/Yae</projectUrl>
<description>Yae Lib</description>
<repository type="git" url="https://github.com/HolographicHat/Yae" commit="$commit$" />
</metadata>
<files>
<file src="build\x64\Release\YaeLib.dll" target="runtimes\win-x64\native" />
</files>
</package>

View File

@@ -20,9 +20,9 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType> <ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset> <PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet> <CharacterSet>MultiByte</CharacterSet>
<UseDebugLibraries>true</UseDebugLibraries>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType> <ConfigurationType>DynamicLibrary</ConfigurationType>
@@ -30,7 +30,6 @@
<PlatformToolset>v143</PlatformToolset> <PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization> <WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet> <CharacterSet>MultiByte</CharacterSet>
<EnableASAN>false</EnableASAN>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings"> <ImportGroup Label="ExtensionSettings">
@@ -45,87 +44,69 @@
</ImportGroup> </ImportGroup>
<PropertyGroup Label="UserMacros" /> <PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir> <OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>build\$(Platform)\$(Configuration)\</IntDir> <IntDir>build\$(Platform)\$(Configuration)\</IntDir>
<TargetName>YaeLib</TargetName> <TargetName>YaeLib</TargetName>
<GenerateManifest>false</GenerateManifest>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir> <OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>build\$(Platform)\$(Configuration)\</IntDir> <IntDir>build\$(Platform)\$(Configuration)\</IntDir>
<GenerateManifest>false</GenerateManifest>
<TargetName>YaeLib</TargetName>
</PropertyGroup> </PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile> <ClCompile>
<WarningLevel>Level3</WarningLevel> <WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck> <SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_DEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode> <ConformanceMode>false</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard> <LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C> <MultiProcessorCompilation>true</MultiProcessorCompilation>
<AdditionalIncludeDirectories>$(ProjectDir)lib\detours\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem>Windows</SubSystem> <SubSystem>NotSet</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>DebugFull</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>$(ProjectDir)lib\detours\;$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>detours-x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent />
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net6.0</Command>
</PostBuildEvent>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile> <ClCompile>
<WarningLevel>Level3</WarningLevel> <WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking> <FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck> <PreprocessorDefinitions>_AMD64_;NDEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>_AMD64_;NDEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions> <ConformanceMode>false</ConformanceMode>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard> <LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
<DebugInformationFormat>None</DebugInformationFormat>
<MultiProcessorCompilation>true</MultiProcessorCompilation> <MultiProcessorCompilation>true</MultiProcessorCompilation>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed> <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<AdditionalIncludeDirectories>$(ProjectDir)lib\detours\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <SDLCheck>true</SDLCheck>
<LanguageStandard_C>stdc11</LanguageStandard_C>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem>Windows</SubSystem> <SubSystem>NotSet</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding> <EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences> <OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>DebugFull</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>$(ProjectDir)lib\detours\;$(OutDir)</AdditionalLibraryDirectories>
<AdditionalDependencies>detours-x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent />
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net8.0-windows\win-x64\YaeAchievementLib.dll /y</Command>
</PostBuildEvent>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="src\HookManager.h" /> <ClInclude Include="src\globals.h" />
<ClInclude Include="src\il2cpp-appdata.h" />
<ClInclude Include="src\il2cpp-functions.h" />
<ClInclude Include="src\il2cpp-types-ptr.h" />
<ClInclude Include="src\il2cpp-types.h" /> <ClInclude Include="src\il2cpp-types.h" />
<ClInclude Include="src\il2cpp-init.h" /> <ClInclude Include="src\il2cpp-init.h" />
<ClInclude Include="src\pch.h" /> <ClInclude Include="src\NamedPipe.h" />
<ClInclude Include="src\ntprivate.h" />
<ClInclude Include="src\util.h" /> <ClInclude Include="src\util.h" />
<ClInclude Include="src\Zydis.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="src\dllmain.cpp" /> <ClCompile Include="src\dllmain.cpp" />
<ClCompile Include="src\il2cpp-init.cpp" /> <ClCompile Include="src\il2cpp-init.cpp" />
<ClCompile Include="src\pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="src\util.cpp" /> <ClCompile Include="src\util.cpp" />
<ClCompile Include="src\Zydis.c" />
</ItemGroup> </ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets"> <ImportGroup Label="ExtensionTargets">

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
//////////////////////////////////////////////////////////////////////////////
//
// Common version parameters.
//
// Microsoft Research Detours Package, Version 4.0.1
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#define _USING_V110_SDK71_ 1
#include "winver.h"
#if 0
#include <windows.h>
#include <detours.h>
#else
#ifndef DETOURS_STRINGIFY
#define DETOURS_STRINGIFY_(x) #x
#define DETOURS_STRINGIFY(x) DETOURS_STRINGIFY_(x)
#endif
#define VER_FILEFLAGSMASK 0x3fL
#define VER_FILEFLAGS 0x0L
#define VER_FILEOS 0x00040004L
#define VER_FILETYPE 0x00000002L
#define VER_FILESUBTYPE 0x00000000L
#endif
#define VER_DETOURS_BITS DETOURS_STRINGIFY(DETOURS_BITS)

View File

@@ -1,66 +0,0 @@
#pragma once
#include "pch.h"
#define CALL_ORIGIN(function, ...) \
HookManager::call(function, __func__, __VA_ARGS__)
class HookManager {
public:
template <typename Fn>
static void install(Fn func, Fn handler) {
enable(func, handler);
holderMap[reinterpret_cast<void*>(handler)] = reinterpret_cast<void*>(func);
}
template <typename Fn>
[[nodiscard]] static Fn getOrigin(Fn handler, const char* callerName = nullptr) noexcept {
if (holderMap.count(reinterpret_cast<void*>(handler)) == 0) {
printf("Origin not found for handler: %s. Maybe racing bug.", callerName == nullptr ? "<Unknown>" : callerName);
return nullptr;
}
return reinterpret_cast<Fn>(holderMap[reinterpret_cast<void*>(handler)]);
}
template <typename Fn>
[[nodiscard]] static void detach(Fn handler) noexcept {
disable(handler);
holderMap.erase(reinterpret_cast<void*>(handler));
}
template <typename RType, typename... Params>
[[nodiscard]] static RType call(RType(*handler)(Params...), const char* callerName = nullptr, Params... params) {
auto origin = getOrigin(handler, callerName);
if (origin != nullptr)
return origin(params...);
return RType();
}
static void detachAll() noexcept {
for (const auto &[key, value] : holderMap) {
disable(key);
}
holderMap.clear();
}
private:
inline static std::map<void*, void*> holderMap{};
template <typename Fn>
static void disable(Fn handler) {
Fn origin = getOrigin(handler);
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)origin, handler);
DetourTransactionCommit();
}
template <typename Fn>
static void enable(Fn& func, Fn handler) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)func, handler);
DetourTransactionCommit();
}
};

46
lib/src/NamedPipe.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <Windows.h>
#include <span>
template <typename T>
concept IsSpan = requires(T t) {
{ t.data() } -> std::convertible_to<const void*>;
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.size_bytes() } -> std::convertible_to<std::size_t>;
};
class NamedPipe
{
HANDLE m_hPipe = INVALID_HANDLE_VALUE;
public:
NamedPipe(HANDLE hPipe) : m_hPipe(hPipe) {}
~NamedPipe() { if (m_hPipe != INVALID_HANDLE_VALUE) CloseHandle(m_hPipe); }
operator HANDLE() const { return m_hPipe; }
operator bool() const { return m_hPipe != INVALID_HANDLE_VALUE && m_hPipe != nullptr; }
NamedPipe& operator= (HANDLE hPipe) {
m_hPipe = hPipe;
return *this;
}
bool Write(const void* data, size_t size) const
{
DWORD bytesWritten;
if (!WriteFile(m_hPipe, data, static_cast<DWORD>(size), &bytesWritten, nullptr) || bytesWritten != size)
return false;
return true;
}
template <IsSpan T>
bool Write(const T& data) const
{
return Write(data.data(), data.size_bytes());
}
template <typename T>
bool Write(const T& data) const
{
return Write(&data, sizeof(T));
}
};

54990
lib/src/Zydis.c Normal file

File diff suppressed because one or more lines are too long

12113
lib/src/Zydis.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,81 +1,275 @@
// ReSharper disable CppCStyleCast // ReSharper disable CppClangTidyCertErr33C
// ReSharper disable CppInconsistentNaming #include <Windows.h>
// ReSharper disable CppClangTidyModernizeUseStdPrint #include <print>
// ReSharper disable CppClangTidyClangDiagnosticCastAlign #include <string>
// ReSharper disable CppClangTidyHicppMultiwayPathsCovered #include <future>
// ReSharper disable CppDefaultCaseNotHandledInSwitchStatement #include <TlHelp32.h>
// ReSharper disable CppClangTidyClangDiagnosticCastFunctionTypeStrict
#include "pch.h" #include "globals.h"
#include "util.h" #include "util.h"
#include "il2cpp-init.h" #include "il2cpp-init.h"
#include "il2cpp-types.h"
#include "ntprivate.h"
using Genshin::ByteArray; CRITICAL_SECTION CriticalSection;
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index);
HWND unityWnd = nullptr; namespace
HANDLE hPipe = nullptr; {
PacketType GetPacketType(const PacketMeta* packet)
{
using namespace Globals;
const auto cmdid = packet->CmdId;
if (AchievementId && cmdid == AchievementId)
return PacketType::Achivement;
std::string checksum; if (AchievementIdSet.contains(cmdid) && packet->DataLength > 500)
return PacketType::Achivement;
if (PlayerStoreId && cmdid == PlayerStoreId)
return PacketType::Inventory;
return PacketType::None;
}
}
namespace Hook { namespace Hook {
ByteArray* UnityEngine_RecordUserData(const INT type) {
if (type == 0) { uint16_t __fastcall BitConverter_ToUInt16(Array<uint8_t>* val, const int startIndex)
const auto arr = new ByteArray {}; {
const auto len = checksum.length(); using namespace Globals;
arr->max_length = len; const auto ToUInt16 = reinterpret_cast<decltype(&BitConverter_ToUInt16)>(Offset.BitConverter_ToUInt16);
memcpy(&arr->vector[0], checksum.data(), len);
return arr; EnterCriticalSection(&CriticalSection);
} SetBreakpoint((HANDLE)-2, 0, false, 0);
return new ByteArray {}; const auto ret = ToUInt16(val, startIndex);
} SetBreakpoint((HANDLE)-2, Offset.BitConverter_ToUInt16, true, 0);
LeaveCriticalSection(&CriticalSection);
uint16_t BitConverter_ToUInt16(ByteArray* val, const int startIndex) {
const auto ret = CALL_ORIGIN(BitConverter_ToUInt16, val, startIndex); if (ret != 0xAB89)
if (ret == 0xAB89 && ReadMapped<UINT16>(val->vector, 2) == 1655) { return ret;
const auto headLength = ReadMapped<UINT16>(val->vector, 4);
const auto dataLength = ReadMapped<UINT32>(val->vector, 6); const auto packet = val->As<PacketMeta*>();
const auto cStr = base64_encode(val->vector + 10 + headLength, dataLength) + "\n"; const auto packetType = GetPacketType(packet);
WriteFile(hPipe, cStr.c_str(), (DWORD) cStr.length(), nullptr, nullptr); if (packetType == PacketType::None)
CloseHandle(hPipe); return ret;
#ifdef _DEBUG
std::println("PacketType: {}", static_cast<uint8_t>(packetType));
std::println("CmdId: {}", packet->CmdId);
std::println("DataLength: {}", packet->DataLength);
//std::println("Data: {}", Util::Base64Encode(packet->AsSpan()));
#endif
if (!MessagePipe.Write(packetType))
Util::Win32ErrorDialog(1002, GetLastError());
if (!MessagePipe.Write(packet->DataLength))
Util::Win32ErrorDialog(1003, GetLastError());
if (!MessagePipe.Write(packet->AsSpan()))
Util::Win32ErrorDialog(1004, GetLastError());
if (!AchievementsWritten)
AchievementsWritten = packetType == PacketType::Achivement;
if (!PlayerStoreWritten)
PlayerStoreWritten = packetType == PacketType::Inventory;
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
{
if (!MessagePipe.Write(PacketType::End))
Util::Win32ErrorDialog(9001, GetLastError());
#ifdef _DEBUG
system("pause");
#endif
ExitProcess(0); ExitProcess(0);
} }
return ret; return ret;
} }
void __fastcall AccountDataItem_UpdateNormalProp(const void* __this, const int type, const double value, const double lastValue, const int state)
{
using namespace Globals;
const auto UpdateNormalProp = reinterpret_cast<decltype(&AccountDataItem_UpdateNormalProp)>(Offset.AccountDataItem_UpdateNormalProp);
EnterCriticalSection(&CriticalSection);
SetBreakpoint((HANDLE)-2, 0, false, 1);
UpdateNormalProp(__this, type, value, lastValue, state);
SetBreakpoint((HANDLE)-2, Offset.AccountDataItem_UpdateNormalProp, true, 1);
LeaveCriticalSection(&CriticalSection);
#ifdef _DEBUG
std::println("PropType: {}", type);
std::println("PropState: {}", state);
std::println("PropValue: {}", value);
std::println("PropLastValue: {}", lastValue);
#endif
if (RequiredPlayerProperties.erase(type) != 0)
{
if (!MessagePipe.Write(PacketType::PropData))
Util::Win32ErrorDialog(2002, GetLastError());
if (!MessagePipe.Write(type))
Util::Win32ErrorDialog(2003, GetLastError());
if (!MessagePipe.Write(value))
Util::Win32ErrorDialog(2004, GetLastError());
}
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
{
if (!MessagePipe.Write(PacketType::End))
Util::Win32ErrorDialog(9001, GetLastError());
#ifdef _DEBUG
system("pause");
#endif
ExitProcess(0);
}
}
} }
void Run(HMODULE* phModule) { LONG __stdcall VectoredExceptionHandler(PEXCEPTION_POINTERS ep)
//AllocConsole(); {
//freopen_s((FILE**)stdout, "CONOUT$", "w", stdout); using namespace Globals;
while ((unityWnd = FindMainWindowByPID(GetCurrentProcessId())) == nullptr) { const auto exceptionRecord = ep->ExceptionRecord;
Sleep(1000); const auto contextRecord = ep->ContextRecord;
if (exceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.BitConverter_ToUInt16)) {
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::BitConverter_ToUInt16);
contextRecord->EFlags &= ~0x100; // clear the trap flag
return EXCEPTION_CONTINUE_EXECUTION;
}
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.AccountDataItem_UpdateNormalProp)) {
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::AccountDataItem_UpdateNormalProp);
contextRecord->EFlags &= ~0x100; // clear the trap flag
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
} }
Sleep(5000);
DisableVMProtect(); return EXCEPTION_CONTINUE_SEARCH;
InitIL2CPP(); }
for (int i = 0; i < 3; i++) {
const auto result = Genshin::RecordUserData(i); void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index)
checksum += string(reinterpret_cast<char*>(&result->vector[0]), result->max_length); {
using namespace Globals;
if (index > 3) {
return;
} }
HookManager::install(Genshin::BitConverter_ToUInt16, Hook::BitConverter_ToUInt16);
HookManager::install(Genshin::UnityEngine_RecordUserData, Hook::UnityEngine_RecordUserData); CONTEXT ctx{};
hPipe = CreateFile(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (hPipe == INVALID_HANDLE_VALUE) { GetThreadContext(thread, &ctx);
Win32ErrorDialog(1001);
DWORD64* dr = &ctx.Dr0;
dr[index] = enable ? address : 0;
const auto mask = 1ull << (index * 2);
ctx.Dr7 |= mask;
SetThreadContext(thread, &ctx);
}
DWORD __stdcall ThreadProc(LPVOID hInstance)
{
#ifdef _DEBUG
AllocConsole();
freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
system("pause");
#endif
InitializeCriticalSection(&CriticalSection);
auto initFuture = std::async(std::launch::async, InitIL2CPP);
using namespace Globals;
const auto pid = GetCurrentProcessId();
while ((GameWindow = Util::FindMainWindowByPID(pid)) == nullptr) {
SwitchToThread();
}
if (!initFuture.get())
ExitProcess(0); ExitProcess(0);
MessagePipe = CreateFileA(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (!MessagePipe)
{
#ifdef _DEBUG
std::println("CreateFile failed: {}", GetLastError());
#else
Util::Win32ErrorDialog(1001, GetLastError());
ExitProcess(0);
#endif
} }
AddVectoredExceptionHandler(1, VectoredExceptionHandler);
while (true)
{
THREADENTRY32 te32{};
te32.dwSize = sizeof(THREADENTRY32);
const auto hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
for (Thread32First(hSnapshot, &te32); Thread32Next(hSnapshot, &te32);)
{
if (te32.th32OwnerProcessID != pid) {
continue;
}
if (const auto hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID))
{
EnterCriticalSection(&CriticalSection);
SetBreakpoint(hThread, Offset.BitConverter_ToUInt16, true, 0);
SetBreakpoint(hThread, Offset.AccountDataItem_UpdateNormalProp, true, 1);
CloseHandle(hThread);
LeaveCriticalSection(&CriticalSection);
}
}
CloseHandle(hSnapshot);
Sleep(1);
}
return 0;
} }
// DLL entry point // DLL entry point
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReasonForCall, LPVOID lpReserved) { BOOL __stdcall DllMain(HMODULE hInstance, DWORD fdwReason, LPVOID lpReserved)
switch (ulReasonForCall) { {
case DLL_PROCESS_ATTACH: // Check injectee
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Run, new HMODULE(hModule), 0, NULL); WCHAR szFileName[MAX_PATH]{};
break; DWORD length = 0;
case DLL_THREAD_ATTACH: GetModuleFileNameW(NULL, szFileName, MAX_PATH);
case DLL_THREAD_DETACH: CharUpperBuffW(szFileName, wcslen(szFileName));
case DLL_PROCESS_DETACH: if (!(wcsstr(szFileName, L"YUANSHEN.EXE") || wcsstr(szFileName, L"GENSHINIMPACT.EXE")))
break; {
return TRUE;
} }
if (fdwReason == DLL_PROCESS_ATTACH)
{
if (hInstance)
{
LdrAddRefDll(LDR_ADDREF_DLL_PIN, hInstance);
}
if (const auto hThread = CreateThread(nullptr, 0, ThreadProc, hInstance, 0, nullptr)) {
CloseHandle(hThread);
}
}
return TRUE; return TRUE;
} }
static LRESULT WINAPI YaeGetWindowHookImpl(int code, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(NULL, code, wParam, lParam);
}
EXTERN_C __declspec(dllexport) HRESULT WINAPI YaeGetWindowHook(_Out_ HOOKPROC* pHookProc)
{
*pHookProc = YaeGetWindowHookImpl;
return S_OK;
}

54
lib/src/globals.h Normal file
View File

@@ -0,0 +1,54 @@
#pragma once
#include <Windows.h>
#include <unordered_set>
#include "NamedPipe.h"
#define PROPERTY2(type, name, cn, os) \
type name##_cn = cn; \
type name##_os = os; \
type get_##name() { return Globals::IsCNREL ? name##_cn : name##_os; } \
void set_##name(type value) { if (Globals::IsCNREL) name##_cn = value; else name##_os = value; } \
__declspec(property(get = get_##name, put = set_##name)) type name;
namespace Globals
{
inline HWND GameWindow = nullptr;
inline NamedPipe MessagePipe = nullptr;
inline bool IsCNREL = true;
inline uintptr_t BaseAddress = 0;
// 5.1.0 - 24082
inline uint16_t AchievementId = 0; // use non-zero to override dynamic search
inline std::unordered_set<uint16_t> AchievementIdSet;
// 5.3.0 - 23233
inline uint16_t PlayerStoreId = 0; // use non-zero to override dynamic search
inline bool AchievementsWritten = false;
inline bool PlayerStoreWritten = false;
/*
* PROP_PLAYER_HCOIN = 10015,
* PROP_PLAYER_WAIT_SUB_HCOIN = 10022,
* PROP_PLAYER_SCOIN = 10016,
* PROP_PLAYER_WAIT_SUB_SCOIN = 10023,
* PROP_PLAYER_MCOIN = 10025,
* PROP_PLAYER_WAIT_SUB_MCOIN = 10026,
* PROP_PLAYER_HOME_COIN = 10042,
* PROP_PLAYER_WAIT_SUB_HOME_COIN = 10043,
* PROP_PLAYER_ROLE_COMBAT_COIN = 10053,
* PROP_PLAYER_MUSIC_GAME_BOOK_COIN = 10058,
*/
inline std::unordered_set<int> RequiredPlayerProperties = { 10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058 };
class Offsets
{
public:
PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0, 0);
//PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0x0F826CF0, 0x0F825F10); // use non-zero to override dynamic search
PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0, 0);
//PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0x0D9FE060, 0x0D94D910); // use non-zero to override dynamic search
};
inline Offsets Offset;
}

View File

@@ -1,19 +0,0 @@
// ReSharper disable CppClangTidyBugproneMacroParentheses
#pragma once
#include "il2cpp-types.h"
// Application-specific functions
#define DO_APP_FUNC(ca, oa, r, n, p) extern r (*n) p
#define DO_UNI_FUNC(ca, oa, r, n, p) extern r (*n) p
namespace Genshin {
#include "il2cpp-functions.h"
}
#undef DO_UNI_FUNC
#undef DO_APP_FUNC
#define DO_TYPEDEF(ca, oa, n) extern n##__Class **n##__TypeInfo
namespace Genshin {
#include "il2cpp-types-ptr.h"
}
#undef DO_TYPEDEF

View File

@@ -1,11 +0,0 @@
using namespace Genshin;
// DO_APP_FUNC(CN_OFFSET, OS_OFFSET, RETURN, FUNC_NAME, (ARGS...));
DO_APP_FUNC(0x57A390, 0x579D00, LPVOID, il2cpp_object_new, (LPVOID t));
DO_APP_FUNC(0x07315A30, 0x0710F580, ByteArray*, RecordUserData, (int32_t nType));
DO_APP_FUNC(0x0D257150, 0x0D244830, uint16_t, BitConverter_ToUInt16, (ByteArray* val, int startIndex));
DO_UNI_FUNC(0x10CF80, 0x10CF80, ByteArray*, UnityEngine_RecordUserData, (int32_t nType));

View File

@@ -1,38 +1,588 @@
// ReSharper disable CppClangTidyBugproneMacroParentheses #include <Windows.h>
#include <print>
#include <string>
#include <vector>
#include <iterator>
#include <algorithm>
#include <ranges>
#include <unordered_set>
#include <unordered_map>
#include <future>
#include <mutex>
#include <immintrin.h>
#include "pch.h" #include "globals.h"
#include "Zydis.h"
#include "util.h"
#include "il2cpp-init.h" namespace
{
class DecodedInstruction
{
public:
DecodedInstruction() = default;
~DecodedInstruction() = default;
DecodedInstruction(const ZydisDecodedInstruction& instruction) : Instruction(instruction) {}
DecodedInstruction(const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : Instruction(instruction) {
Operands = { operands, operands + operandCount };
}
DecodedInstruction(const uint32_t rva, const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : RVA(rva), Instruction(instruction) {
Operands = { operands, operands + operandCount };
}
// copy constructor
DecodedInstruction(const DecodedInstruction& other) = default;
// move constructor
DecodedInstruction(DecodedInstruction&& other) noexcept : RVA(other.RVA), Instruction(other.Instruction), Operands(std::move(other.Operands)) {}
uint32_t RVA = 0;
ZydisDecodedInstruction Instruction;
std::vector<ZydisDecodedOperand> Operands;
};
std::span<uint8_t> GetSection(LPCSTR name)
{
using namespace Globals;
if (BaseAddress == 0)
return {};
const auto dosHeader = (PIMAGE_DOS_HEADER)BaseAddress;
const auto ntHeader = (PIMAGE_NT_HEADERS)((uintptr_t)dosHeader + dosHeader->e_lfanew);
const auto sectionHeader = IMAGE_FIRST_SECTION(ntHeader);
for (auto i = 0; i < ntHeader->FileHeader.NumberOfSections; i++)
{
if (strcmp((char*)sectionHeader[i].Name, name) == 0)
{
const auto sectionSize = sectionHeader[i].Misc.VirtualSize;
const auto virtualAddress = BaseAddress + sectionHeader[i].VirtualAddress;
return std::span(reinterpret_cast<uint8_t*>(virtualAddress), sectionSize);
}
}
return {};
}
/// <summary>
/// decodes all instruction until next push, ignores branching
/// </summary>
/// <param name="address"></param>
/// <param name="maxInstructions"></param>
/// <returns>std::vector DecodedInstruction</returns>
std::vector<DecodedInstruction> DecodeFunction(uintptr_t address, int32_t maxInstructions = -1)
{
using namespace Globals;
std::vector<DecodedInstruction> instructions;
ZydisDecoder decoder{};
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
ZydisDecodedInstruction instruction{};
ZydisDecoderContext context{};
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT_VISIBLE]{};
while (true)
{
const auto data = reinterpret_cast<uint8_t*>(address);
auto status = ZydisDecoderDecodeInstruction(&decoder, &context, data, ZYDIS_MAX_INSTRUCTION_LENGTH, &instruction);
if (!ZYAN_SUCCESS(status))
{
// for skipping jump tables
address += 1;
continue;
}
status = ZydisDecoderDecodeOperands(&decoder, &context, &instruction, operands, instruction.operand_count_visible);
if (!ZYAN_SUCCESS(status))
{
// for skipping jump tables
address += 1;
continue;
}
if (instruction.mnemonic == ZYDIS_MNEMONIC_PUSH && !instructions.empty()) {
break;
}
const auto rva = static_cast<uint32_t>(address - BaseAddress);
instructions.emplace_back(rva, instruction, operands, instruction.operand_count_visible);
address += instruction.length;
if (maxInstructions != -1 && instructions.size() >= maxInstructions)
break;
}
return instructions;
}
/// <summary>
/// get the count of data references in the instructions (only second oprand of mov)
/// </summary>
/// <param name="instructions"></param>
/// <returns></returns>
int32_t GetDataReferenceCount(const std::vector<DecodedInstruction>& instructions)
{
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
if (instr.Instruction.mnemonic != ZYDIS_MNEMONIC_MOV)
return false;
if (instr.Operands.size() != 2)
return false;
const auto& op = instr.Operands[1];
// access to memory, based off of rip, 32-bit displacement
return op.type == ZYDIS_OPERAND_TYPE_MEMORY && op.mem.base == ZYDIS_REGISTER_RIP && op.mem.disp.has_displacement;
}));
}
int32_t GetCallCount(const std::vector<DecodedInstruction>& instructions)
{
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL;
}));
}
int32_t GetUniqueCallCount(const std::vector<DecodedInstruction>& instructions)
{
std::unordered_set<uint32_t> calls;
for (const auto& instr : instructions)
{
if (instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL) {
uint32_t destination = instr.Operands[0].imm.value.s + instr.RVA + instr.Instruction.length;
calls.insert(destination);
}
}
return static_cast<int32_t>(calls.size());
}
int32_t GetCmpImmCount(const std::vector<DecodedInstruction>& instructions)
{
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP && instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && instr.Operands[1].imm.value.u;
}));
}
void ResolveAchivementCmdId()
{
if (Globals::AchievementId != 0)
return;
const auto il2cppSection = GetSection("il2cpp");
std::println("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
if (il2cppSection.empty())
return; // message box?
const auto candidates = Util::PatternScanAll(il2cppSection, "56 48 83 EC 20 48 89 D0 48 89 CE 80 3D ? ? ? ? 00");
std::println("Candidates: {}", candidates.size());
std::vector<std::vector<DecodedInstruction>> filteredInstructions;
std::ranges::copy_if(
candidates | std::views::transform([](auto va) { return DecodeFunction(va); }),
std::back_inserter(filteredInstructions),
[](const std::vector<DecodedInstruction>& instr) {
return GetDataReferenceCount(instr) == 5 && GetCallCount(instr) == 10 &&
GetUniqueCallCount(instr) == 6 && GetCmpImmCount(instr) == 5;
});
// should have only one result
if (filteredInstructions.size() != 1)
{
std::println("Filtered Instructions: {}", filteredInstructions.size());
return;
}
const auto& instructions = filteredInstructions[0];
std::println("RVA: 0x{:08X}", instructions.front().RVA);
// extract all the non-zero immediate values from the cmp instructions
std::vector<uint32_t> cmdIds;
std::ranges::for_each(instructions, [&cmdIds](const DecodedInstruction& instr) {
if (instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP &&
instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
instr.Operands[1].imm.value.u != 0) {
cmdIds.push_back(static_cast<uint32_t>(instr.Operands[1].imm.value.u));
}
});
for (const auto& cmdId : cmdIds)
{
std::println("AchievementId: {}", cmdId);
Globals::AchievementIdSet.insert(static_cast<uint16_t>(cmdId));
}
}
std::vector<uintptr_t> GetCalls(uint8_t* target)
{
const auto il2cppSection = GetSection("il2cpp");
const auto sectionAddress = reinterpret_cast<uintptr_t>(il2cppSection.data());
const auto sectionSize = il2cppSection.size();
std::vector<uintptr_t> callSites;
const __m128i callOpcode = _mm_set1_epi8(0xE8);
const size_t simdEnd = sectionSize / 16 * 16;
for (size_t i = 0; i < simdEnd; i += 16) {
// load 16 bytes from the current address
const __m128i chunk = _mm_loadu_si128((__m128i*)(sectionAddress + i));
// compare the loaded chunk with 0xE8 in all 16 bytes
const __m128i result = _mm_cmpeq_epi8(chunk, callOpcode);
// move the comparison results into a mask
int mask = _mm_movemask_epi8(result);
while (mask != 0) {
DWORD first_match_idx = 0;
_BitScanForward(&first_match_idx, mask); // index of the first set bit (match)
// index of the instruction
const size_t instruction_index = i + first_match_idx;
const int32_t delta = *(int32_t*)(sectionAddress + instruction_index + 1);
const uintptr_t dest = sectionAddress + instruction_index + 5 + delta;
if (dest == (uintptr_t)target) {
callSites.push_back(sectionAddress + instruction_index);
}
// clear the bit we just processed and continue with the next match
mask &= ~(1 << first_match_idx);
}
}
return callSites;
}
uintptr_t FindFunctionEntry(uintptr_t address) // not a correct way to find function entry
{
__try
{
while (true)
{
// go back to 'sub rsp' instruction
uint32_t code = *(uint32_t*)address;
code &= ~0xFF000000;
if (_byteswap_ulong(code) == 0x4883EC00) { // sub rsp, ??
return address;
}
address--;
}
}
__except (1) {}
return address;
}
void Resolve_BitConverter_ToUInt16()
{
if (Globals::Offset.BitConverter_ToUInt16 != 0) {
Globals::Offset.BitConverter_ToUInt16 += Globals::BaseAddress;
return;
}
const auto il2cppSection = GetSection("il2cpp");
std::print("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
/*
mov ecx, 0Fh
call ThrowHelper.ThrowArgumentNullException
mov ecx, 0Eh
mov edx, 16h
call ThrowHelper.ThrowArgumentOutOfRangeException
mov ecx, 5
call ThrowHelper.ThrowArgumentException
*/
auto candidates = Util::PatternScanAll(il2cppSection, "B9 0F 00 00 00 E8 ? ? ? ? B9 0E 00 00 00 BA 16 00 00 00 E8 ? ? ? ? B9 05 00 00 00 E8 ? ? ? ?");
std::println("Candidates: {}", candidates.size());
std::vector<uintptr_t> filteredEntries;
std::ranges::copy_if(candidates, std::back_inserter(filteredEntries), [](uintptr_t& entry) {
entry = FindFunctionEntry(entry);
return entry % 16 == 0;
});
for (const auto& entry : filteredEntries)
{
std::println("Entry: 0x{:X}", entry);
}
std::println("Looking for call counts...");
std::mutex mutex;
std::unordered_map<uintptr_t, int32_t> callCounts;
// find the call counts to candidate functions
std::vector<std::future<void>> futures;
std::ranges::transform(filteredEntries, std::back_inserter(futures), [&](uintptr_t entry) {
return std::async(std::launch::async, [&](uintptr_t e) {
const auto callSites = GetCalls((uint8_t*)e);
std::lock_guard lock(mutex);
callCounts[e] = callSites.size();
}, entry);
});
for (auto& future : futures) {
future.get();
}
uintptr_t targetEntry = 0;
for (const auto& [entry, count] : callCounts)
{
std::println("Entry: 0x{:X}, RVA: 0x{:08X}, Count: {}", entry, entry - Globals::BaseAddress, count);
if (count == 3) {
targetEntry = entry;
}
}
Globals::Offset.BitConverter_ToUInt16 = targetEntry;
}
void ResolveInventoryCmdId()
{
if (Globals::PlayerStoreId != 0)
return;
const auto il2cppSection = GetSection("il2cpp");
std::println("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
/*
cmp r8d, 2
jz 0x3B
cmd r8d, 1
mov rax
*/
// look for ItemModule.GetBagManagerByStoreType <- mf got inlined in 5.5
// we just gon to look for OnPlayerStoreNotify
const auto candidates = Util::PatternScanAll(il2cppSection, "41 83 F8 02 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 41 83 F8 01");
std::println("Candidates: {}", candidates.size());
if (candidates.empty())
return;
uintptr_t pOnPlayerStoreNotify = 0;
{
// one of the candidates is OnPlayerStoreNotify
// search after the pattern to find an arbirary branch
auto decodedInstructions = candidates | std::views::transform([](auto va) { return DecodeFunction(va, 20); });
// find the call site with an arbitrary branch (JMP or CALL) after the call
auto targetInstructions = std::ranges::find_if(decodedInstructions, [](const auto& instr) {
return std::ranges::any_of(instr, [](const DecodedInstruction& i) {
return (i.Instruction.mnemonic == ZYDIS_MNEMONIC_JMP || i.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL) &&
i.Operands.size() == 1 && i.Operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER;
});
});
if (targetInstructions == decodedInstructions.end()) {
std::println("Failed to find target instruction");
return;
}
// ItemModule.OnPlayerStoreNotify
const auto& instructions = *targetInstructions;
pOnPlayerStoreNotify = Globals::BaseAddress + instructions.front().RVA;
const auto isFunctionEntry = [](uintptr_t va) -> bool {
auto* code = reinterpret_cast<uint8_t*>(va);
return (va % 16 == 0 &&
code[0] == 0x56 && // push rsi
(*reinterpret_cast<uint32_t*>(&code[1]) & ~0xFF000000) == _byteswap_ulong(0x4883EC00)); // sub rsp, ??
};
auto range = std::views::iota(0, 126);
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(pOnPlayerStoreNotify - i); });
it != range.end())
{
pOnPlayerStoreNotify -= *it;
}
else {
std::println("Failed to find function entry");
return;
}
std::println("OnPlayerStoreNotify: 0x{:X}", pOnPlayerStoreNotify);
}
uintptr_t pOnPacket = 0;
{
// get all calls to OnPlayerStoreNotify
const auto calls = GetCalls(reinterpret_cast<uint8_t*>(pOnPlayerStoreNotify));
if (calls.size() != 1) {
std::println("Failed to find call site");
return;
}
// ItemModule.OnPacket - search backwards for function entry
pOnPacket = calls.front();
const auto isFunctionEntry = [](uintptr_t va) -> bool {
auto* code = reinterpret_cast<uint8_t*>(va);
return (va % 16 == 0 &&
code[0] == 0x56 && // push rsi
(*reinterpret_cast<uint32_t*>(&code[1]) & ~0xFF000000) == _byteswap_ulong(0x4883EC00)); // sub rsp, ??
};
auto range = std::views::iota(0, 3044);
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(pOnPacket - i); });
it != range.end())
{
pOnPacket -= *it;
}
else {
std::println("Failed to find function entry");
return;
}
std::println("OnPacket: 0x{:X}", pOnPacket);
}
const auto decodedInstructions = DecodeFunction(pOnPacket);
uint32_t cmdid = 0;
std::ranges::for_each(decodedInstructions, [&cmdid, pOnPlayerStoreNotify](const DecodedInstruction& i) {
static uint32_t immValue = 0; // keep track of the last immediate value
if (i.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP &&
i.Operands.size() == 2 &&
i.Operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
i.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE)
{
immValue = static_cast<uint32_t>(i.Operands[1].imm.value.u);
}
if (i.Instruction.meta.branch_type == ZYDIS_BRANCH_TYPE_NEAR && i.Operands.size() == 1 &&
(i.Instruction.mnemonic == ZYDIS_MNEMONIC_JZ || i.Instruction.mnemonic == ZYDIS_MNEMONIC_JNZ)) // jz for true branch, jnz for false branch
{
// assume the branching is jz
uintptr_t branchAddr = Globals::BaseAddress + i.RVA + i.Instruction.length + i.Operands[0].imm.value.s;
// check if the branch is jnz and adjust the branch address
if (i.Instruction.mnemonic == ZYDIS_MNEMONIC_JNZ) {
branchAddr = Globals::BaseAddress + i.RVA + i.Instruction.length;
}
// decode the branch address immediately
const auto instructions = DecodeFunction(branchAddr, 10);
const auto isMatch = std::ranges::any_of(instructions, [pOnPlayerStoreNotify](const DecodedInstruction& instr) {
if (instr.Instruction.mnemonic != ZYDIS_MNEMONIC_CALL)
return false;
uintptr_t destination = 0;
ZydisCalcAbsoluteAddress(&instr.Instruction, instr.Operands.data(), Globals::BaseAddress + instr.RVA, &destination);
return destination == pOnPlayerStoreNotify;
});
if (isMatch) {
cmdid = immValue;
}
}
return cmdid == 0; // stop processing if cmdid is found
});
Globals::PlayerStoreId = static_cast<uint16_t>(cmdid);
std::println("PlayerStoreId: {}", Globals::PlayerStoreId);
}
void Resolve_AccountDataItem_UpdateNormalProp()
{
if (Globals::Offset.AccountDataItem_UpdateNormalProp != 0) {
Globals::Offset.AccountDataItem_UpdateNormalProp += Globals::BaseAddress;
return;
}
const auto il2cppSection = GetSection("il2cpp");
/*
add ??, 0FFFFD8EEh
cmp ??, 30h
*/
auto candidates = Util::PatternScanAll(il2cppSection, "81 ? EE D8 FF FF ? 83 ? 30");
// should have only one result
if (candidates.size() != 1)
{
std::println("Filtered Instructions: {}", candidates.size());
return;
}
auto fp = candidates[0];
const auto isFunctionEntry = [](uintptr_t va) -> bool {
auto* code = reinterpret_cast<uint8_t*>(va);
/* push rsi */
/* push rdi */
return (va % 16 == 0 && code[0] == 0x56 && code[1] == 0x57);
};
auto range = std::views::iota(0, 213);
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(fp - i); }); it != range.end()) {
fp -= *it;
} else {
std::println("Failed to find function entry");
return;
}
Globals::Offset.AccountDataItem_UpdateNormalProp = fp;
}
#define DO_APP_FUNC(ca, oa, r, n, p) r (*n) p
#define DO_UNI_FUNC(ca, oa, r, n, p) r (*n) p
namespace Genshin {
#include "il2cpp-functions.h"
} }
#undef DO_UNI_FUNC
#undef DO_APP_FUNC
#define DO_TYPEDEF(ca, oa, n) n##__Class **n##__TypeInfo bool InitIL2CPP()
namespace Genshin { {
#include "il2cpp-types-ptr.h" std::string buffer;
} buffer.resize(MAX_PATH);
#undef DO_TYPEDEF ZeroMemory(buffer.data(), MAX_PATH);
const auto pathLength = GetModuleFileNameA(nullptr, buffer.data(), MAX_PATH);
using std::string; if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
void InitIL2CPP() { buffer.resize(pathLength);
TCHAR szFileName[MAX_PATH]; ZeroMemory(buffer.data(), pathLength);
GetModuleFileName(NULL, szFileName, MAX_PATH); GetModuleFileNameA(nullptr, buffer.data(), pathLength);
auto isCN = strstr(szFileName, "YuanShen.exe");//string(szFileName).contains(); }
auto hBase = GetModuleHandle("UserAssembly.dll"); buffer.shrink_to_fit();
auto bAddr = (UINT64)hBase;
auto cAddr = (UINT64)GetModuleHandle("UnityPlayer.dll"); using namespace Globals;
#define DO_APP_FUNC(ca, oa, r, n, p) n = (r (*) p)(bAddr + (isCN ? ca : oa)) IsCNREL = buffer.find("YuanShen.exe") != std::string::npos;
#define DO_UNI_FUNC(ca, oa, r, n, p) n = (r (*) p)(cAddr + (isCN ? ca : oa)) BaseAddress = (uintptr_t)GetModuleHandleA(nullptr);
#include "il2cpp-functions.h"
#undef DO_UNI_FUNC std::future<void> resolveFuncFuture = std::async(std::launch::async, Resolve_BitConverter_ToUInt16);
#undef DO_APP_FUNC std::future<void> resolveCmdIdFuture = std::async(std::launch::async, ResolveAchivementCmdId);
#define DO_TYPEDEF(ca, oa, n) n##__TypeInfo = (n##__Class **)(bAddr + (isCN ? ca : oa)) std::future<void> resolveInventoryFuture = std::async(std::launch::async, ResolveInventoryCmdId);
#include "il2cpp-types-ptr.h" std::future<void> resolveUpdatePropFuture = std::async(std::launch::async, Resolve_AccountDataItem_UpdateNormalProp);
#undef DO_TYPEDEF
resolveFuncFuture.get();
resolveCmdIdFuture.get();
resolveInventoryFuture.get();
resolveUpdatePropFuture.get();
std::println("BaseAddress: 0x{:X}", BaseAddress);
std::println("IsCNREL: {:d}", IsCNREL);
std::println("BitConverter_ToUInt16: 0x{:X}", Offset.BitConverter_ToUInt16);
std::println("AccountDataItem_UpdateNormalProp: 0x{:X}", Offset.AccountDataItem_UpdateNormalProp);
if (!AchievementId && AchievementIdSet.empty())
{
Util::ErrorDialog("Failed to resolve achievement data");
return false;
}
if (!PlayerStoreId)
{
Util::ErrorDialog("Failed to resolve inventory data");
return false;
}
return true;
} }

View File

@@ -1,4 +1,4 @@
#pragma once #pragma once
// IL2CPP application initializer // IL2CPP application initializer
void InitIL2CPP(); bool InitIL2CPP();

View File

@@ -1,45 +1,78 @@
// ReSharper disable CppClangTidyClangDiagnosticReservedIdentifier
// ReSharper disable CppClangTidyBugproneReservedIdentifier
#pragma once #pragma once
#include <cstdint>
#include <span>
#pragma region IL2CPPInternalTypes #define PROPERTY_GET_CONST(type, name, funcBody) \
type get_##name() const funcBody \
__declspec(property(get = get_##name)) type name;
typedef uint16_t Il2CppChar; enum class PacketType : uint8_t
typedef uintptr_t il2cpp_array_size_t; {
typedef int32_t il2cpp_array_lower_bound_t; None = 0,
Achivement = 1,
Inventory = 2,
PropData = 100,
End = 255,
};
typedef struct Il2CppObject { template <typename T>
union { class Array
void* klass; {
void* vtable; public:
} Il2CppClass; void* klass;
void* monitor; void* monitor;
} Il2CppObject; void* bounds;
size_t max_length;
T vector[1];
typedef struct Il2CppString { Array() = delete;
Il2CppObject object;
int32_t length;
Il2CppChar chars[32];
} Il2CppString;
typedef struct Il2CppArrayBounds { T* data() {
il2cpp_array_size_t length; return vector;
il2cpp_array_lower_bound_t lower_bound; }
} Il2CppArrayBounds;
#pragma endregion std::span<T> AsSpan() {
return { vector, max_length };
}
namespace Genshin { template <typename U>
U As() {
return reinterpret_cast<U>(vector);
}
};
struct ByteArray { static_assert(alignof(Array<uint8_t>) == 8, "Array alignment is incorrect");
void* klass; static_assert(offsetof(Array<uint8_t>, vector) == 32, "vector offset is incorrect");
void* monitor;
Il2CppArrayBounds* bounds; #pragma pack(push, 1)
il2cpp_array_size_t max_length; class PacketMeta
uint8_t vector[32]; {
}; uint16_t m_HeadMagic;
uint16_t m_CmdId;
struct CodedOutputStream__Class { uint16_t m_HeaderLength;
}; uint32_t m_DataLength;
} uint8_t m_Data[1];
public:
PacketMeta() = delete;
PROPERTY_GET_CONST(uint16_t, HeadMagic, { return _byteswap_ushort(m_HeadMagic); });
PROPERTY_GET_CONST(uint16_t, CmdId, { return _byteswap_ushort(m_CmdId); });
PROPERTY_GET_CONST(uint16_t, HeaderLength, { return _byteswap_ushort(m_HeaderLength); });
PROPERTY_GET_CONST(uint32_t, DataLength, { return _byteswap_ulong(m_DataLength); });
std::span<uint8_t> AsSpan() {
return { m_Data + HeaderLength, DataLength };
}
friend struct PacketMetaStaticAssertHelper;
};
#pragma pack(pop)
struct PacketMetaStaticAssertHelper
{
static_assert(offsetof(PacketMeta, m_CmdId) == 2, "CmdId offset is incorrect");
static_assert(offsetof(PacketMeta, m_HeaderLength) == 4, "HeadLength offset is incorrect");
static_assert(offsetof(PacketMeta, m_DataLength) == 6, "DataLength offset is incorrect");
static_assert(offsetof(PacketMeta, m_Data) == 10, "Data offset is incorrect");
};

9
lib/src/ntprivate.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include <Windows.h>
#include <bcrypt.h>
#pragma comment(lib, "ntdll.lib")
#define LDR_ADDREF_DLL_PIN 0x00000001
EXTERN_C NTSYSAPI NTSTATUS NTAPI LdrAddRefDll(_In_ ULONG Flags, _In_ PVOID DllHandle);

View File

@@ -1 +0,0 @@
#include "pch.h"

View File

@@ -1,27 +0,0 @@
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include <windows.h>
// 添加要在此处预编译的标头
#include <map>
#include <set>
#include <string>
#include <vector>
#include <codecvt>
#include <cstdint>
#include <iostream>
#include <detours.h>
#include "HookManager.h"
#include "il2cpp-appdata.h"
#endif //PCH_H

View File

@@ -1,105 +1,210 @@
#include "pch.h" #include <string>
#include <array>
#include <ranges>
#include <intrin.h>
#include "util.h" #include "util.h"
VOID DisableVMProtect() { #include "globals.h"
DWORD oldProtect = 0;
auto ntdll = GetModuleHandleA("ntdll.dll");
auto pNtProtectVirtualMemory = GetProcAddress(ntdll, "NtProtectVirtualMemory");
auto pNtQuerySection = GetProcAddress(ntdll, "NtQuerySection");
DWORD old;
VirtualProtect(pNtProtectVirtualMemory, 1, PAGE_EXECUTE_READWRITE, &old);
*(uintptr_t*)pNtProtectVirtualMemory = *(uintptr_t*)pNtQuerySection & ~(0xFFui64 << 32) | (uintptr_t)(*(uint32_t*)((uintptr_t)pNtQuerySection + 4) - 1) << 32;
VirtualProtect(pNtProtectVirtualMemory, 1, old, &old);
}
#pragma region StringConvert #ifdef _DEBUG
#pragma runtime_checks("", off)
string ToString(Il2CppString* str, UINT codePage) { #endif
auto chars = reinterpret_cast<const wchar_t*>(str->chars);
auto len = WideCharToMultiByte(codePage, 0, chars, -1, nullptr, 0, nullptr, nullptr);
auto buffer = new char[len];
WideCharToMultiByte(codePage, 0, chars, -1, buffer, len, nullptr, nullptr);
return string(buffer);
}
#pragma endregion
#pragma region ByteUtils
bool IsLittleEndian() {
UINT i = 1;
char* c = (char*)&i;
return (*c);
}
#pragma endregion
#pragma region FindMainWindowByPID #pragma region FindMainWindowByPID
struct HandleData { namespace
DWORD pid; {
HWND hwnd; struct HandleData {
}; DWORD Pid;
HWND Hwnd;
};
BOOL IsMainWindow(HWND handle) { bool IsMainWindow(HWND handle) {
return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle) == TRUE; return GetWindow(handle, GW_OWNER) == nullptr && IsWindowVisible(handle) == TRUE;
} }
BOOL IsUnityWindow(HWND handle) { bool IsUnityWindow(HWND handle) {
TCHAR name[256]; char szName[256]{};
GetClassName(handle, name, 256); GetClassNameA(handle, szName, 256);
return _strcmpi(name, "UnityWndClass") == 0; return _stricmp(szName, "UnityWndClass") == 0;
} }
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) { BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
HandleData& data = *(HandleData*)lParam; HandleData& data = *(HandleData*)lParam;
DWORD pid = 0; DWORD pid = 0;
GetWindowThreadProcessId(handle, &pid); GetWindowThreadProcessId(handle, &pid);
if (data.pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle)) if (data.Pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle))
return TRUE; return TRUE;
data.hwnd = handle; data.Hwnd = handle;
return FALSE; return FALSE;
} }
std::tuple<std::vector<uint8_t>, std::string> PatternToBytes(const char* pattern)
{
std::vector<uint8_t> bytes;
std::string mask;
const auto start = const_cast<char*>(pattern);
const auto end = const_cast<char*>(pattern) + strlen(pattern);
for (auto current = start; current < end; ++current) {
if (*current == '?') {
++current;
if (*current == '?')
++current;
bytes.push_back(-1);
mask.push_back('?');
}
else {
bytes.push_back(strtoul(current, &current, 16));
mask.push_back('x');
}
}
return { bytes, mask };
}
HWND FindMainWindowByPID(DWORD pid) {
HandleData data = { pid, 0 };
EnumWindows(EnumWindowsCallback, (LPARAM)&data);
return data.hwnd;
} }
#pragma endregion #pragma endregion
static const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static constexpr LPCSTR base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string base64_encode(BYTE const* buf, unsigned int bufLen) { namespace Util
std::string ret; {
int i = 0; HWND FindMainWindowByPID(DWORD pid)
BYTE char_array_3[3]; {
BYTE char_array_4[4]; HandleData data = {
while (bufLen--) { .Pid = pid,
char_array_3[i++] = *buf++; .Hwnd = nullptr
if (i == 3) { };
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; EnumWindows(EnumWindowsCallback, (LPARAM)&data);
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); return data.Hwnd;
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); }
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; (i < 4); i++) std::string Base64Encode(std::span<uint8_t> data)
ret += base64_chars[char_array_4[i]]; {
i = 0; return Base64Encode(data.data(), data.size());
} }
}
if (i) { std::string Base64Encode(uint8_t const* buf, size_t bufLen)
int j; {
for (j = i; j < 3; j++) std::string ret;
char_array_3[j] = '\0'; int i = 0;
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; uint8_t char_array_3[3];
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); uint8_t char_array_4[4];
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); while (bufLen--) {
char_array_4[3] = char_array_3[2] & 0x3f; char_array_3[i++] = *buf++;
for (j = 0; j < i + 1; j++) if (i == 3) {
ret += base64_chars[char_array_4[j]]; char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
while (i++ < 3) char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
ret += '='; char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
} char_array_4[3] = char_array_3[2] & 0x3f;
return ret; for (i = 0; (i < 4); i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
int j;
for (j = i; j < 3; j++)
char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (j = 0; j < i + 1; j++)
ret += base64_chars[char_array_4[j]];
while (i++ < 3)
ret += '=';
}
return ret;
}
void ErrorDialog(LPCSTR title, LPCSTR msg)
{
MessageBoxA(Globals::GameWindow, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL);
}
void ErrorDialog(LPCSTR msg)
{
ErrorDialog("YaeAchievement", msg);
}
void Win32ErrorDialog(DWORD code, DWORD winerrcode)
{
const std::string msg = "CRITICAL ERROR!\nError code: " + std::to_string(winerrcode) + "-" + std::to_string(code) +
"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!";
ErrorDialog("YaeAchievement", msg.c_str());
}
std::vector<uintptr_t> PatternScanAll(std::span<uint8_t> bytes, const char* pattern)
{
std::vector<uintptr_t> results;
const auto [patternBytes, patternMask] = PatternToBytes(pattern);
constexpr std::size_t chunkSize = 16;
const auto maskCount = static_cast<std::size_t>(std::ceil(patternMask.size() / chunkSize));
std::array<int32_t, 32> masks{};
auto chunks = patternMask | std::views::chunk(chunkSize);
for (std::size_t i = 0; auto chunk : chunks) {
int32_t mask = 0;
for (std::size_t j = 0; j < chunk.size(); ++j) {
if (chunk[j] == 'x') {
mask |= 1 << j;
}
}
masks[i++] = mask;
}
__m128i xmm1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(patternBytes.data()));
__m128i xmm2, xmm3, mask;
auto pData = bytes.data();
const auto end = pData + bytes.size() - patternMask.size();
while (pData < end)
{
_mm_prefetch(reinterpret_cast<const char*>(pData + 64), _MM_HINT_NTA);
if (patternBytes[0] == pData[0])
{
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pData));
mask = _mm_cmpeq_epi8(xmm1, xmm2);
if ((_mm_movemask_epi8(mask) & masks[0]) == masks[0])
{
bool found = true;
for (int i = 1; i < maskCount; ++i)
{
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pData + i * chunkSize));
xmm3 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(patternBytes.data() + i * chunkSize));
mask = _mm_cmpeq_epi8(xmm2, xmm3);
if ((_mm_movemask_epi8(mask) & masks[i]) != masks[i])
{
found = false;
break;
}
}
if (found) {
results.push_back(reinterpret_cast<uintptr_t>(pData));
}
}
}
++pData;
}
return results;
}
} }
#ifdef _DEBUG
#pragma runtime_checks("", restore)
#endif

View File

@@ -1,26 +1,19 @@
// ReSharper disable CppClangTidyClangDiagnosticLanguageExtensionToken
#pragma once #pragma once
#include <Windows.h>
#include <type_traits>
#include <vector>
#include <span>
using std::string; namespace Util
{
HWND FindMainWindowByPID(DWORD pid);
std::string Base64Encode(std::span<uint8_t> data);
std::string Base64Encode(uint8_t const* buf, size_t bufLen);
VOID DisableVMProtect(); void ErrorDialog(LPCSTR title, LPCSTR msg);
bool IsLittleEndian(); void ErrorDialog(LPCSTR msg);
HWND FindMainWindowByPID(DWORD pid); void Win32ErrorDialog(DWORD code, DWORD winerrcode);
string ToString(Il2CppString* str, UINT codePage = CP_ACP);
std::string base64_encode(BYTE const* buf, unsigned int bufLen);
#define ErrorDialogT(title, msg) MessageBox(unityWnd, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL); std::vector<uintptr_t> PatternScanAll(std::span<uint8_t> bytes, const char* pattern);
#define ErrorDialog(msg) ErrorDialogT("YaeAchievement", msg) }
#define Win32ErrorDialog(code) ErrorDialogT("YaeAchievement", ("CRITICAL ERROR!\nError code: " + std::to_string(GetLastError()) + "-"#code"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!").c_str())
template<class T>
static T ReadMapped(void* data, int offset, bool littleEndian = false) {
char* cData = (char*)data;
T result = {};
if (IsLittleEndian() != littleEndian) {
for (int i = 0; i < sizeof(T); i++)
((char*)&result)[i] = cData[offset + sizeof(T) - i - 1];
return result;
}
memcpy(&result, cData + offset, sizeof(result));
return result;
}

Binary file not shown.

View File

@@ -1,21 +0,0 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message Achievement {
enum Status {
INVALID = 0;
UNFINISHED = 1;
FINISHED = 2;
REWARD_TAKEN = 3;
}
uint32 timestamp = 13;
uint32 current = 6;
uint32 total = 9;
uint32 id = 1;
Status status = 2;
}
message AchievementAllDataNotify {
repeated Achievement list = 11;
}

View File

@@ -1,15 +0,0 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message UpdateInfo {
uint32 versionCode = 1;
string versionName = 2;
string description = 3;
string packageLink = 4;
bool forceUpdate = 5;
bool enableLibDownload = 6;
bool enableAutoDownload = 7;
string currentCNGameHash = 8;
string currentOSGameHash = 9;
}

View File

@@ -1,66 +0,0 @@
using System.Text.RegularExpressions;
using YaeAchievement.res;
namespace YaeAchievement;
public static partial class AppConfig {
public static string GamePath { get; private set; } = null!;
internal static void Load(string argumentPath) {
if (argumentPath != "auto" && File.Exists(argumentPath)) {
GamePath = argumentPath;
return;
}
var pathCacheFile = new CacheFile("genshin_impact_game_path");
if (pathCacheFile.Exists()) {
var path = pathCacheFile.Read().Content.ToStringUtf8();
if (path != null && File.Exists(path)) {
GamePath = path;
return;
}
}
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var cnLogPath = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\原神\output_log.txt");
var osLogPath = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\Genshin Impact\output_log.txt");
if (!File.Exists(cnLogPath) && !File.Exists(osLogPath)) {
throw new ApplicationException(App.ConfigNeedStartGenshin);
}
string finalLogPath;
if (!File.Exists(osLogPath)) {
finalLogPath = cnLogPath;
} else if (!File.Exists(cnLogPath)) {
finalLogPath = osLogPath;
} else {
var cnLastWriteTime = File.GetLastWriteTime(cnLogPath);
var osLastWriteTime = File.GetLastWriteTime(osLogPath);
finalLogPath = cnLastWriteTime > osLastWriteTime ? cnLogPath : osLogPath;
}
GamePath = GetGamePathFromLogFile(finalLogPath)
?? GetGamePathFromLogFile($"{finalLogPath}.last")
?? throw new ApplicationException(App.ConfigNeedStartGenshin);
pathCacheFile.Write(GamePath);
}
private static string? GetGamePathFromLogFile(string path) {
if (!File.Exists(path)) {
return null;
}
var copiedLogFilePath = Path.GetTempFileName();
File.Copy(path, copiedLogFilePath, true);
var content = File.ReadAllText(copiedLogFilePath);
try {
File.Delete(copiedLogFilePath);
} catch (Exception) { /* ignore */}
var matchResult = GamePathRegex().Match(content);
if (!matchResult.Success) {
return null;
}
var entryName = matchResult.Groups["1"].Value.Replace("_Data", ".exe");
return Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
}
[GeneratedRegex(@"(?m).:/.+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
private static partial Regex GamePathRegex();
}

View File

@@ -1,39 +0,0 @@
using System.IO.Compression;
using Google.Protobuf;
using Proto;
namespace YaeAchievement;
public class CacheFile(string identifier) {
private readonly string _cacheName = Path.Combine(GlobalVars.CachePath, $"{identifier.MD5Hash()[..16]}.miko");
private CacheItem? _content;
public DateTime LastWriteTime => Exists() ? File.GetLastWriteTimeUtc(_cacheName) : DateTime.UnixEpoch;
public bool Exists() => File.Exists(_cacheName);
public CacheItem Read() {
if (_content == null) {
using var fInput = File.OpenRead(_cacheName);
using var dInput = new GZipStream(fInput, CompressionMode.Decompress);
_content = CacheItem.Parser.ParseFrom(dInput);
}
return _content;
}
public void Write(string data, string? etag = null) => Write(ByteString.CopyFromUtf8(data), data.MD5Hash(), etag);
public void Write(byte[] data, string? etag = null) => Write(ByteString.CopyFrom(data), data.MD5Hash(), etag);
private void Write(ByteString data, string hash, string? etag) {
using var fOut = File.OpenWrite(_cacheName);
using var cOut = new GZipStream(fOut, CompressionLevel.SmallestSize);
new CacheItem {
Etag = etag ?? string.Empty,
Version = 3,
Checksum = hash,
Content = data
}.WriteTo(cOut);
}
}

View File

@@ -1,263 +0,0 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Win32;
using Proto;
using YaeAchievement.res;
using static Proto.Achievement.Types;
namespace YaeAchievement;
public static class Export {
public static uint ExportTo { get; set; } = uint.MaxValue;
private static readonly JsonSerializerOptions JsonOpts = new () {
WriteIndented = true
};
public static void Choose(AchievementAllDataNotify data) {
if (ExportTo == uint.MaxValue) {
Console.Write(App.ExportChoose);
while (Console.KeyAvailable) {
Console.ReadKey(false);
}
if (!uint.TryParse(Console.ReadLine(), out var num)) num = 0;
ExportTo = num;
}
((Action<AchievementAllDataNotify>) (ExportTo switch {
1 => ToHuTao,
2 => ToPaimon,
3 => ToSeelie,
4 => ToCSV,
5 => ToXunkong,
6 => ToWxApp1,
7 => ToTeyvatGuide,
8 => ToUIAFJson,
9 => ToRawJson,
_ => ToCocogoat
})).Invoke(data);
}
private class CocogoatResponse {
[JsonPropertyName("key")] public string Code { get; init; } = null!;
}
private static void ToCocogoat(AchievementAllDataNotify data) {
var result = JsonSerializer.Serialize(ExportToUIAFApp(data));
using var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
request.RequestUri = new Uri($"https://77.cocogoat.cn/v1/memo?source={App.AllAchievement}");
request.Content = new StringContent(result, Encoding.UTF8, "application/json");
using var response = Utils.CHttpClient.Send(request);
if (response.StatusCode != HttpStatusCode.Created) {
Console.WriteLine(App.ExportToCocogoatFail);
return;
}
var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var responseJson = JsonSerializer.Deserialize<CocogoatResponse>(responseText)!;
Console.WriteLine(Utils.ShellOpen($"https://cocogoat.work/achievement?memo={responseJson.Code}")
? App.ExportToCocogoatSuccess
: $"https://cocogoat.work/achievement?memo={responseJson.Code}");
}
private static void ToWxApp1(AchievementAllDataNotify data) {
var id = Guid.NewGuid().ToString("N").Substring(20, 8);
var result = JsonSerializer.Serialize(new Dictionary<string, object> {
{ "key", id },
{ "data", ExportToUIAFApp(data) }
});
using var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
request.RequestUri = new Uri("https://api.qyinter.com/achievementRedis");
request.Content = new StringContent(result, Encoding.UTF8, "application/json");
using var response = Utils.CHttpClient.Send(request);
Console.WriteLine(App.ExportToWxApp1Success, id);
}
private static void ToHuTao(AchievementAllDataNotify data) {
if (CheckWinUIAppScheme("hutao")) {
Utils.CopyToClipboard(JsonSerializer.Serialize(ExportToUIAFApp(data)));
Utils.ShellOpen("hutao://achievement/import");
Console.WriteLine(App.ExportToSnapGenshinSuccess);
} else {
Console.WriteLine(App.ExportToSnapGenshinNeedUpdate);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9PH4NXJ2JN52");
}
}
private static void ToXunkong(AchievementAllDataNotify data) {
if (CheckWinUIAppScheme("xunkong")) {
Utils.CopyToClipboard(JsonSerializer.Serialize(ExportToUIAFApp(data)));
Utils.ShellOpen("xunkong://import-achievement?caller=YaeAchievement&from=clipboard");
Console.WriteLine(App.ExportToXunkongSuccess);
} else {
Console.WriteLine(App.ExportToXunkongNeedUpdate);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9N2SVG0JMT12");
}
}
private static void ToTeyvatGuide(AchievementAllDataNotify data) {
if (Process.GetProcessesByName("TeyvatGuide").Length != 0) {
Utils.CopyToClipboard(JsonSerializer.Serialize(ExportToUIAFApp(data)));
Utils.ShellOpen("teyvatguide://import_uigf?app=YaeAchievement");
Console.WriteLine(App.ExportToTauriSuccess);
} else {
Console.WriteLine(App.ExportToTauriFail);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9NLBNNNBNSJN");
}
}
// ReSharper disable once InconsistentNaming
private static void ToUIAFJson(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"uiaf-{DateTime.Now:yyyyMMddHHmmss}.json");
if (TryWriteToFile(path, JsonSerializer.Serialize(ExportToUIAFApp(data)))) {
Console.WriteLine(App.ExportToFileSuccess, path);
}
}
private static void ToPaimon(AchievementAllDataNotify data) {
var info = LoadAchievementInfo();
var output = new Dictionary<uint, Dictionary<uint, bool>>();
foreach (var ach in data.List.Where(a => a.Status is Status.Finished or Status.RewardTaken)) {
if (!info.Items.TryGetValue(ach.Id, out var achInfo) || achInfo == null) {
Console.WriteLine($@"Unable to find {ach.Id} in metadata.");
continue;
}
var map = output.GetValueOrDefault(achInfo.Group, new Dictionary<uint, bool>());
map[ach.Id == 81222 ? 81219 : ach.Id] = true;
output[achInfo.Group] = map;
}
var final = new Dictionary<string, Dictionary<uint, Dictionary<uint, bool>>> {
["achievement"] = output.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value)
};
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-paimon.json");
if (TryWriteToFile(path, JsonSerializer.Serialize(final))) {
Console.WriteLine(App.ExportToFileSuccess, path);
}
}
private static void ToSeelie(AchievementAllDataNotify data) {
var output = new Dictionary<uint, Dictionary<string, bool>>();
foreach (var ach in data.List.Where(a => a.Status is Status.Finished or Status.RewardTaken)) {
output[ach.Id == 81222 ? 81219 : ach.Id] = new Dictionary<string, bool> {
["done"] = true
};
}
var final = new Dictionary<string, Dictionary<uint, Dictionary<string, bool>>> {
["achievements"] = output.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value)
};
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-seelie.json");
if (TryWriteToFile(path, JsonSerializer.Serialize(final))) {
Console.WriteLine(App.ExportToFileSuccess, path);
}
}
// ReSharper disable once InconsistentNaming
private static void ToCSV(AchievementAllDataNotify data) {
var info = LoadAchievementInfo();
var outList = new List<List<object>>();
foreach (var ach in data.List.OrderBy(a => a.Id)) {
if (UnusedAchievement.Contains(ach.Id)) continue;
if (!info.Items.TryGetValue(ach.Id, out var achInfo) || achInfo == null) {
Console.WriteLine($@"Unable to find {ach.Id} in metadata.");
continue;
}
var finishAt = "";
if (ach.Timestamp != 0) {
var ts = Convert.ToInt64(ach.Timestamp);
finishAt = DateTimeOffset.FromUnixTimeSeconds(ts).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
}
var current = ach.Status != Status.Unfinished ? ach.Current == 0 ? ach.Total : ach.Current : ach.Current;
outList.Add([
ach.Id, ach.Status.ToDesc(), achInfo.Group, achInfo.Name,
achInfo.Description, current, ach.Total, finishAt
]);
}
var output = new List<string> { "ID,状态,特辑,名称,描述,当前进度,目标进度,完成时间" };
output.AddRange(outList.OrderBy(v => v[2]).Select(item => {
item[2] = info.Group[(uint) item[2]];
return item.JoinToString(",");
}));
var path = Path.GetFullPath($"achievement-{DateTime.Now:yyyyMMddHHmmss}.csv");
if (TryWriteToFile(path, $"\uFEFF{string.Join("\n", output)}")) {
Console.WriteLine(App.ExportToFileSuccess, path);
Process.Start("explorer.exe", $"{Path.GetDirectoryName(path)}");
}
}
private static void ToRawJson(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-raw.json");
var text = JsonSerializer.Serialize(data, JsonOpts);
if (TryWriteToFile(path, text)) {
Console.WriteLine(App.ExportToFileSuccess, path);
}
}
// ReSharper disable once InconsistentNaming
private static Dictionary<string, object> ExportToUIAFApp(AchievementAllDataNotify data) {
var output = data.List
.Where(a => (uint)a.Status > 1 || a.Current > 0)
.Select(ach => new Dictionary<string, uint> {
["id"] = ach.Id,
["status"] = (uint) ach.Status,
["current"] = ach.Current,
["timestamp"] = ach.Timestamp
})
.ToList();
return new Dictionary<string, object> {
["info"] = new Dictionary<string, object> {
["export_app"] = "YaeAchievement",
["export_timestamp"] = DateTimeOffset.Now.ToUnixTimeSeconds(),
["export_app_version"] = GlobalVars.AppVersionName,
["uiaf_version"] = "v1.1"
},
["list"] = output
};
}
// ReSharper disable once InconsistentNaming
private static bool CheckWinUIAppScheme(string protocol) {
return (string?)Registry.ClassesRoot.OpenSubKey(protocol)?.GetValue("") == $"URL:{protocol}";
}
private static string JoinToString(this IEnumerable<object> list, string separator) {
return string.Join(separator, list);
}
private static readonly List<uint> UnusedAchievement = [ 84517 ];
private static string ToDesc(this Status status) {
return status switch {
Status.Invalid => App.StatusInvalid,
Status.Finished => App.StatusFinished,
Status.Unfinished => App.StatusUnfinished,
Status.RewardTaken => App.StatusRewardTaken,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
};
}
private static AchievementInfo LoadAchievementInfo() {
var b = Utils.GetBucketFileAsByteArray("schicksal/metadata");
return AchievementInfo.Parser.ParseFrom(b);
}
public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) {
// ReSharper disable once LocalizableElement
Console.WriteLine($"{msg}: {ex.Message}");
return ex.NativeErrorCode;
}
private static bool TryWriteToFile(string path, string contents) {
try {
File.WriteAllText(path, contents);
return true;
} catch (UnauthorizedAccessException) {
Console.WriteLine(App.NoWritePermission, path);
return false;
}
}
}

View File

@@ -1,41 +0,0 @@
using System.Security.Cryptography;
using System.Text;
namespace YaeAchievement;
// ReSharper disable MemberCanBePrivate.Global
public static class Extensions {
// ReSharper disable once InconsistentNaming
private static readonly Lazy<MD5> md5 = new (MD5.Create);
// ReSharper disable once InconsistentNaming
private static readonly Lazy<SHA1> sha1 = new (SHA1.Create);
public static byte[] ToBytes(this string text) {
return Encoding.UTF8.GetBytes(text);
}
// ReSharper disable once InconsistentNaming
public static string MD5Hash(this string text) {
return text.ToBytes().MD5Hash();
}
// ReSharper disable once InconsistentNaming
public static string MD5Hash(this byte[] data) {
return md5.Value.ComputeHash(data).ToHex().ToLower();
}
// ReSharper disable once InconsistentNaming
public static string SHA1Hash(this string text, bool base64 = true) {
var bytes = sha1.Value.ComputeHash(text.ToBytes());
return base64 ? bytes.ToBase64() : bytes.ToHex();
}
public static string ToHex(this byte[] bytes) {
return Convert.ToHexString(bytes);
}
public static string ToBase64(this byte[] bytes) {
return Convert.ToBase64String(bytes);
}
}

View File

@@ -1,62 +0,0 @@
using System.ComponentModel;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Memory;
using Windows.Win32.System.Threading;
namespace YaeAchievement;
public static class Injector {
public static unsafe bool CreateProcess(string path, out HANDLE hProc, out HANDLE hThread, out uint pid) {
Span<char> cmdLines = stackalloc char[1]; // "\0"
var si = new STARTUPINFOW {
cb = unchecked((uint)sizeof(STARTUPINFOW))
};
var dir = Path.GetDirectoryName(path)!;
var result = Native.CreateProcess(
path, ref cmdLines, default, default, false,
PROCESS_CREATION_FLAGS.CREATE_SUSPENDED, default, dir, in si, out var pi
);
pid = pi.dwProcessId;
hProc = pi.hProcess;
hThread = pi.hThread;
return result;
}
// todo: refactor
public static unsafe int LoadLibraryAndInject(HANDLE hProc, ReadOnlySpan<byte> libPath) {
fixed (char* lpModelName = "kernel32.dll") {
var hKernel = Native.GetModuleHandle(lpModelName);
if (hKernel.IsNull) {
return new Win32Exception().PrintMsgAndReturnErrCode("GetModuleHandle fail");
}
fixed(byte* lpProcName = "LoadLibraryA"u8) {
var pLoadLibrary = Native.GetProcAddress(hKernel, (PCSTR)lpProcName);
if (pLoadLibrary.IsNull) {
return new Win32Exception().PrintMsgAndReturnErrCode("GetProcAddress fail");
}
var pBase = Native.VirtualAllocEx(hProc, default, unchecked((uint)libPath.Length + 1), VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT, PAGE_PROTECTION_FLAGS.PAGE_READWRITE);
if ((nint)pBase == 0) {
return new Win32Exception().PrintMsgAndReturnErrCode("VirtualAllocEx fail");
}
fixed (void* lpBuffer = libPath) {
if (!Native.WriteProcessMemory(hProc, pBase, lpBuffer, unchecked((uint)libPath.Length))) {
return new Win32Exception().PrintMsgAndReturnErrCode("WriteProcessMemory fail");
}
}
var lpStartAddress = (delegate* unmanaged[Stdcall]<void*, uint>)pLoadLibrary.Value; //THREAD_START_ROUTINE
var hThread = Native.CreateRemoteThread(hProc, default, 0, lpStartAddress, pBase, 0);
if (hThread.IsNull) {
var e = new Win32Exception();
Native.VirtualFreeEx(hProc, pBase, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE);
return e.PrintMsgAndReturnErrCode("CreateRemoteThread fail");
}
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
Native.VirtualFreeEx(hProc, pBase, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE);
}
return !Native.CloseHandle(hThread) ? new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail") : 0;
}
}
}
}

View File

@@ -1,46 +0,0 @@
using Proto;
using YaeAchievement;
using YaeAchievement.res;
using static YaeAchievement.Utils;
TryDisableQuickEdit();
InstallExitHook();
InstallExceptionHook();
CheckSelfIsRunning();
await CheckVcRuntime();
CheckGenshinIsRunning();
Console.WriteLine(@"----------------------------------------------------");
Console.WriteLine(App.AppBanner, GlobalVars.AppVersionName);
Console.WriteLine(@"https://github.com/HolographicHat/YaeAchievement");
Console.WriteLine(@"----------------------------------------------------");
AppConfig.Load(args.GetOrNull(0) ?? "auto");
Export.ExportTo = ToUIntOrNull(args.GetOrNull(1)) ?? uint.MaxValue;
CheckUpdate(ToBooleanOrFalse(args.GetOrNull(2)));
var historyCache = new CacheFile("ExportData");
AchievementAllDataNotify? data = null;
try {
data = AchievementAllDataNotify.Parser.ParseFrom(historyCache.Read().Content);
} catch (Exception) { /* ignored */ }
if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null) {
Console.WriteLine(App.UsePreviousData);
if (Console.ReadLine()?.ToUpper() is "Y" or "YES") {
Export.Choose(data);
return;
}
}
StartAndWaitResult(AppConfig.GamePath, str => {
GlobalVars.UnexpectedExit = false;
var bytes = Convert.FromBase64String(str);
var list = AchievementAllDataNotify.Parser.ParseFrom(bytes);
historyCache.Write(bytes);
Export.Choose(list);
return true;
});

View File

@@ -1,318 +0,0 @@
using Microsoft.Win32;
using System.ComponentModel;
using System.Diagnostics;
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using Proto;
using YaeAchievement.res;
namespace YaeAchievement;
public static class Utils {
public static readonly HttpClient CHttpClient = new (new HttpClientHandler {
Proxy = GlobalVars.DebugProxy ? new WebProxy("http://127.0.0.1:8888") : null,
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
}) {
DefaultRequestHeaders = {
UserAgent = {
new ProductInfoHeaderValue("YaeAchievement", GlobalVars.AppVersion.ToString(2))
}
}
};
public static byte[] GetBucketFileAsByteArray(string path, bool cache = true) {
try {
using var msg = new HttpRequestMessage();
msg.Method = HttpMethod.Get;
msg.RequestUri = new Uri($"{GlobalVars.BucketHost}/{path}");
var cacheFile = new CacheFile(path);
if (cache && cacheFile.Exists()) {
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cacheFile.Read().Etag}");
}
using var response = CHttpClient.Send(msg);
if (cache && response.StatusCode == HttpStatusCode.NotModified) {
return cacheFile.Read().Content.ToByteArray();
}
response.EnsureSuccessStatusCode();
var responseBytes = response.Content.ReadAsByteArrayAsync().Result;
if (cache) {
var etag = response.Headers.ETag!.Tag;
cacheFile.Write(responseBytes, etag);
}
return responseBytes;
} catch (Exception e) when(e is SocketException or TaskCanceledException) {
Console.WriteLine(App.NetworkError, e.Message);
Environment.Exit(-1);
return null!;
}
}
public static T? GetOrNull<T>(this T[] array, uint index) where T : class {
return array.Length > index ? array[index] : null;
}
public static uint? ToUIntOrNull(string? value) {
return value != null ? uint.TryParse(value, out var result) ? result : null : null;
}
public static bool ToBooleanOrFalse(string? value) {
return value != null && bool.TryParse(value, out var result) && result;
}
public static unsafe void CopyToClipboard(string text) {
if (Native.OpenClipboard(HWND.Null))
{
Native.EmptyClipboard();
var hGlobal = (HGLOBAL) Marshal.AllocHGlobal((text.Length + 1) * 2);
var hPtr = (nint) Native.GlobalLock(hGlobal);
Marshal.Copy(text.ToCharArray(), 0, hPtr, text.Length);
Native.GlobalUnlock((HGLOBAL) hPtr);
Native.SetClipboardData(13, new HANDLE(hPtr));
Marshal.FreeHGlobal(hGlobal);
Native.CloseClipboard();
}
else
{
throw new Win32Exception();
}
}
// ReSharper disable once NotAccessedField.Local
private static UpdateInfo _updateInfo = null!;
public static void CheckUpdate(bool useLocalLib) {
var info = UpdateInfo.Parser.ParseFrom(GetBucketFileAsByteArray("schicksal/version"))!;
if (GlobalVars.AppVersionCode < info.VersionCode) {
Console.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, info.VersionName);
Console.WriteLine(App.UpdateDescription, info.Description);
if (info.EnableAutoDownload) {
Console.WriteLine(App.UpdateDownloading);
var tmpPath = Path.GetTempFileName();
File.WriteAllBytes(tmpPath, GetBucketFileAsByteArray(info.PackageLink));
var updaterArgs = $"{Environment.ProcessId}|{Environment.ProcessPath}|{tmpPath}";
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
File.WriteAllBytes(updaterPath, App.Updater);
ShellOpen(updaterPath, updaterArgs.ToBytes().ToBase64());
GlobalVars.PauseOnExit = false;
Environment.Exit(0);
}
Console.WriteLine(App.DownloadLink, info.PackageLink);
if (info.ForceUpdate) {
Environment.Exit(0);
}
}
if (useLocalLib) {
Console.WriteLine(@"[DEBUG] Use local native lib.");
File.Copy(Path.Combine(GlobalVars.AppPath, "YaeAchievementLib.dll"), GlobalVars.LibFilePath, true);
} else if (info.EnableLibDownload) {
File.WriteAllBytes(GlobalVars.LibFilePath, GetBucketFileAsByteArray("schicksal/lib.dll"));
}
_updateInfo = info;
}
public static void CheckSelfIsRunning() {
try {
Process.EnterDebugMode();
var cur = Process.GetCurrentProcess();
foreach (var process in Process.GetProcesses().Where(process => process.Id != cur.Id)) {
if (process.ProcessName == cur.ProcessName) {
Console.WriteLine(App.AnotherInstance);
Environment.Exit(302);
}
}
Process.LeaveDebugMode();
} catch (Win32Exception) {}
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static bool ShellOpen(string path, string? args = null) {
try {
var startInfo = new ProcessStartInfo {
FileName = path,
UseShellExecute = true
};
if (args != null) {
startInfo.Arguments = args;
}
return new Process {
StartInfo = startInfo
}.Start();
} catch (Exception) {
return false;
}
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static unsafe bool TryDisableQuickEdit() {
var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
CONSOLE_MODE mode = default;
return Native.GetConsoleMode(handle, &mode) && Native.SetConsoleMode(handle, mode & ~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE);
}
public static void CheckGenshinIsRunning() {
Process.EnterDebugMode();
foreach (var process in Process.GetProcesses()) {
if (process.ProcessName is "GenshinImpact" or "YuanShen"
&& !process.HasExited
&& process.MainWindowHandle != nint.Zero
) {
Console.WriteLine(App.GenshinIsRunning, process.Id);
Environment.Exit(301);
}
}
Process.LeaveDebugMode();
}
// ReSharper disable once InconsistentNaming
private static Process? proc;
public static void InstallExitHook() {
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
proc?.Kill();
if (GlobalVars.PauseOnExit) {
Console.WriteLine(App.PressKeyToExit);
Console.ReadKey();
}
};
}
public static void InstallExceptionHook() {
AppDomain.CurrentDomain.UnhandledException += (_, e) => {
var ex = e.ExceptionObject;
switch (ex) {
case ApplicationException ex1:
Console.WriteLine(ex1.Message);
break;
case SocketException ex2:
Console.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
break;
case HttpRequestException ex3:
Console.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
break;
default:
Console.WriteLine(ex.ToString());
break;
}
Environment.Exit(-1);
};
}
private static bool CheckGenshinIsLatestVersion(string path) {
#if DEBUG
return true;
#else
if (!File.Exists(path)) return false;
var hash = File.ReadAllBytes(path).MD5Hash();
return hash == _updateInfo.CurrentCNGameHash || hash == _updateInfo.CurrentOSGameHash;
#endif
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static Thread StartAndWaitResult(string exePath, Func<string, bool> onReceive) {
if (!CheckGenshinIsLatestVersion(exePath)) {
Console.WriteLine(App.GenshinHashError);
Environment.Exit(0);
}
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
try {
File.Delete(GlobalVars.LibFilePath);
} catch (Exception) { /* ignored */ }
};
if (!Injector.CreateProcess(exePath, out var hProcess, out var hThread, out var pid)) {
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("ICreateProcess fail"));
}
if (Injector.LoadLibraryAndInject(hProcess,Encoding.UTF8.GetBytes(GlobalVars.LibFilePath)) != 0)
{
if (!Native.TerminateProcess(hProcess, 0))
{
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail"));
}
}
Console.WriteLine(App.GameLoading, pid);
proc = Process.GetProcessById(Convert.ToInt32(pid));
proc.EnableRaisingEvents = true;
proc.Exited += (_, _) => {
if (GlobalVars.UnexpectedExit)
{
proc = null;
Console.WriteLine(App.GameProcessExit);
Environment.Exit(114514);
}
};
if (Native.ResumeThread(hThread) == 0xFFFFFFFF)
{
var e = new Win32Exception();
if (!Native.TerminateProcess(hProcess, 0))
{
new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail");
}
Environment.Exit(e.PrintMsgAndReturnErrCode("ResumeThread fail"));
}
if (!Native.CloseHandle(hProcess))
{
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail"));
}
var ts = new ThreadStart(() => {
var server = new NamedPipeServerStream(GlobalVars.PipeName);
server.WaitForConnection();
using var reader = new StreamReader(server);
while (!proc.HasExited) {
var line = reader.ReadLine();
if (line?.Length > 0) {
if (onReceive(line)) {
break;
}
server.Disconnect();
server.WaitForConnection();
}
}
});
var th = new Thread(ts);
th.Start();
return th;
}
public static async Task CheckVcRuntime() {
using var root = Registry.LocalMachine;
using var sub = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")!;
var installed = sub.GetSubKeyNames()
.Select(subKeyName => {
try {
return sub.OpenSubKey(subKeyName);
} catch (Exception) {
return null;
}
})
.Select(item => item?.GetValue("DisplayName") as string ?? string.Empty)
.Any(name => name.Contains("Microsoft Visual C++ 2022 X64 "));
if (!installed) {
Console.WriteLine(App.VcRuntimeDownload);
var pkgPath = Path.Combine(GlobalVars.DataPath, "vc_redist.x64.exe");
byte[] bytes;
try {
bytes = await CHttpClient.GetByteArrayAsync("https://aka.ms/vs/17/release/vc_redist.x64.exe");
} catch (Exception e) when(e is SocketException or TaskCanceledException) {
Console.WriteLine(App.NetworkError, e.Message);
Environment.Exit(-1);
return;
}
await File.WriteAllBytesAsync(pkgPath, bytes);
Console.WriteLine(App.VcRuntimeInstalling);
using var process = new Process();
process.StartInfo.FileName = pkgPath;
process.StartInfo.Arguments = "/install /passive /norestart";
process.Start();
await process.WaitForExitAsync();
File.Delete(pkgPath);
}
}
}