108 Commits
4.1.0 ... 5.7.2

Author SHA1 Message Date
HolographicHat
19f84bdb86 bump version 2025-10-22 06:23:04 +08:00
HolographicHat
9d22fdf6f9 implement WhenFirstSuccessful method for improved task handling 2025-10-22 06:21:22 +08:00
HolographicHat
27fa0d9c84 move srcgen project 2025-10-22 01:46:58 +08:00
HolographicHat
e9baf8f211 [skip ci] update ci 2025-10-18 20:40:18 +08:00
HolographicHat
b960165b7e Merge pull request #144 from Lightczx/master 2025-10-14 16:17:21 +08:00
DismissedLight
a4c2027ada code style 2025-10-14 13:44:51 +08:00
DismissedLight
f49477c49a Generated MinHook.Attach methods 2025-10-14 11:30:58 +08:00
HolographicHat
67c2fb3bda bump lib version 2025-10-10 18:20:13 +08:00
HolographicHat
be3440695d auto enter gate (close #143) 2025-09-16 11:42:49 +08:00
HolographicHat
f8b8a5a9e1 #142 2025-08-30 11:39:41 +08:00
HolographicHat
49f8679996 bump version 2025-08-05 16:46:56 +08:00
HolographicHat
222a26233e mirror 2025-08-05 01:45:02 +08:00
HolographicHat
1130f442fc update native lib 2025-08-05 00:10:13 +08:00
HolographicHat
b80987f574 [skip ci] update ci 2025-08-04 22:38:29 +08:00
HolographicHat
c96395e1a2 call awake in wndhook 2025-08-04 22:27:48 +08:00
HolographicHat
3f42156b20 ReadAtLeast 2025-08-04 16:19:33 +08:00
HolographicHat
45638b7327 update ci 2025-08-03 14:13:08 +08:00
HolographicHat
d514f3b5e7 [skip ci] update ci 2025-08-03 13:51:55 +08:00
HolographicHat
3fe54d908e add wndhook 2025-08-03 13:46:09 +08:00
HolographicHat
48043a9deb remove test code 2025-08-02 11:26:03 +08:00
HolographicHat
c09e008d38 bump version 2025-08-02 11:20:10 +08:00
HolographicHat
ac818735c7 suppress pipe broken exception 2025-08-02 08:18:09 +08:00
HolographicHat
c4fb5a0366 invoke libMain by CRT
Co-authored-by: HolographicHat <holographichat@outlook.com>
Co-authored-by: 34736384 <34736384@users.noreply.github.com>
2025-08-02 07:48:44 +08:00
HolographicHat
0522078582 bump version 2025-08-02 02:17:36 +08:00
HolographicHat
97d063ec76 update dependencies 2025-08-02 02:05:14 +08:00
HolographicHat
a6caa07599 update ci & remove old native lib 2025-08-02 02:03:04 +08:00
HolographicHat
7fc296f6e9 refactor native lib 2025-08-02 01:58:01 +08:00
HolographicHat
1216ffb872 [skip ci] update join group link 2025-08-01 15:34:24 +08:00
HolographicHat
f8341c3044 Update ci 2025-06-25 01:04:25 +08:00
HolographicHat
40269410c5 fix sentry#2707 2025-06-23 00:03:12 +08:00
HolographicHat
a32e27323c fix sentry#2715 2025-06-22 23:15:34 +08:00
HolographicHat
4a1da61904 bump version 2025-06-18 14:44:37 +08:00
HolographicHat
9eb8955fda optimize GetGamePath 2025-06-11 01:47:34 +08:00
HolographicHat
62c08f54ab fix 2025-06-08 00:44:32 +08:00
HolographicHat
645fe38c65 fix #138 2025-06-07 20:35:24 +08:00
HolographicHat
8f9a26a237 fix #2471 2025-06-05 21:56:03 +08:00
HolographicHat
8648b3a308 bump version 2025-06-04 03:16:09 +08:00
HolographicHat
829553b3a6 sentry 2025-06-04 03:14:07 +08:00
HolographicHat
87898eedfa fix 2025-06-04 02:01:00 +08:00
HolographicHat
a10b491886 prefer using field id from config 2025-05-30 01:23:39 +08:00
HolographicHat
e3e7107b14 Update YaeAchievementLib.nuspec 2025-05-08 10:11:05 +08:00
HolographicHat
3231746aa5 Merge pull request #135 from 34736384/master 2025-05-07 15:44:48 +08:00
34736384
5c9cdd46d2 update psn pattern 2025-05-07 13:34:52 +08:00
HolographicHat
881a4bc725 [no ci] update docs 2025-04-13 03:49:58 +08:00
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
81 changed files with 3125 additions and 2983 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

@@ -1,6 +1,7 @@
name: .NET Build name: .NET Build
on: on:
workflow_dispatch:
push: push:
branches: [ "master" ] branches: [ "master" ]
pull_request: pull_request:
@@ -8,23 +9,32 @@ on:
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
defaults:
run:
working-directory: ./YaeAchievement
steps: steps:
- uses: actions/checkout@v4 - name: Checkout Repo
uses: actions/checkout@v4
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 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
run: dotnet build -c Release --no-restore run: dotnet build -c Release --no-restore
- name: Publish - name: Publish-AOT
run: dotnet publish --property:OutputPath=.\publish\ run: dotnet publish --property:OutputPath=.\publish\
- name: Upload artifact - name: Upload-AOT
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Artifacts name: aot
path: publish path: YaeAchievement\publish\publish
- name: Publish-NoAOT
run: dotnet publish --property:OutputPath=.\naot-publish\ --property:PublishAot=false --property:PublishSingleFile=true --property:PublishTrimmed=true
- name: Upload-NoAOT
uses: actions/upload-artifact@v4
with:
name: normal
path: YaeAchievement\naot-publish\publish

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

@@ -0,0 +1,36 @@
name: YaeLib NuGet Publish
on:
workflow_dispatch:
inputs:
confirm_version:
description: 'Version already increased?'
required: true
type: boolean
perform_publish:
description: 'Publish to nuget?'
required: true
default: true
type: boolean
jobs:
publish:
runs-on: windows-latest
defaults:
run:
working-directory: YaeAchievementLib
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Build native library
run: dotnet publish
- name: Publish to NuGet
if: ${{ inputs.perform_publish }}
run: nuget push bin\Release\*.nupkg ${{ secrets.NUGET_API_KEY }} -src https://api.nuget.org/v3/index.json
- name: Upload nuget package
uses: actions/upload-artifact@v4
with:
name: nupkg
path: YaeAchievementLib\bin\Release\*.nupkg

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,4 +1,4 @@
<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
@@ -13,8 +13,6 @@
## 导出支持 ## 导出支持
> 按照数字键选择导出方式,<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/)
@@ -32,11 +30,8 @@
[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/q/mCQ8PwpxJY)
## 常见问题 ## 常见问题
0. Q: 打不开
A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer)
1. Q: 原神启动时报错: 数据异常(31-4302) 1. Q: 原神启动时报错: 数据异常(31-4302)
A: 不要把软件和原神主程序放一起 A: 不要把软件和原神主程序放一起

View File

@@ -1,4 +1,4 @@
<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
@@ -14,8 +14,6 @@
## Export support ## Export support
> Select the export method according to the number keys, <kbd>0</kbd> is the default export method
0. [Cocogoat](https://cocogoat.work/achievement) 0. [Cocogoat](https://cocogoat.work/achievement)
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/)
@@ -32,13 +30,9 @@
## 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/q/mCQ8PwpxJY)
## Frequently asked questions ## Frequently asked questions
0. Q: Unable to start
A: Download and install [.NET Runtime 7](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer) or ` winget install Microsoft.DotNet.Runtime.8`
1. Q: Error while Genshin started: Data Exception (31-4302) 1. Q: Error while Genshin started: Data Exception (31-4302)
A: Don't place software in the directory containing Genshin Impact. A: Don't place software in the directory containing Genshin Impact.

View File

@@ -1,4 +1,4 @@
<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
@@ -14,8 +14,6 @@
## エクスポートサポート ## エクスポートサポート
> 数字キーに従ってエクスポート方法を選択します。<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/)
@@ -32,13 +30,9 @@
## ダウンロード: [こちら](https://github.com/HolographicHat/YaeAchievement/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/q/mCQ8PwpxJY)
## よくある質問 ## よくある質問
0. Q: 起動できない
A: [.NET Runtime 7](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer) をダウンロードしてインストールするか、`winget install Microsoft.DotNet.Runtime.8` を実行してください。
1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302) 1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302)
A: ソフトウェアを原神のディレクトリに配置しないでください。 A: ソフトウェアを原神のディレクトリに配置しないでください。

View File

@@ -6,31 +6,19 @@
点击图中红框圈中的名称为“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-8.0.4-windows-x64-installer 。
进入网页后浏览器会自动弹出下载,同样地,将文件保存在桌面或者其它易于寻找的文件夹内。
下载完成后打开名称形如dotnet-runtime-x.x.x-win-x64.exe的文件会弹出安装窗口如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/2.png)
直接点击安装即可。
3.打开主程序所需的操作以及成就导出的选择
双击在第一步下载的名称为“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 +26,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 +62,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,39 +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)
Click Herehttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer .
Or `winget install Microsoft.DotNet.Runtime.7` 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.
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)
Just click Install.
3.The actions required to open the main program and the options for the achievement export 3.The actions required to open the main program and the options for the achievement export
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 +27,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 +55,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)

View File

@@ -6,33 +6,19 @@
赤枠で囲まれた「YaeAchievement.exe」という名前のファイルをクリックすると、自動的にポップアップしてダウンロードされます。このファイルをデスクトップや他の見やすいフォルダに保存することをお勧めします。 赤枠で囲まれた「YaeAchievement.exe」という名前のファイルをクリックすると、自動的にポップアップしてダウンロードされます。このファイルをデスクトップや他の見やすいフォルダに保存することをお勧めします。
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide1.png) ![Guide1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3)
2. .NET Runtime 7をインストールランタイムが既にインストールされている場合はこのステップを無視できます
こちらをクリックhttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer .
または、Windows 11を使用しているか、Wingetがインストールされている場合は、`winget install Microsoft.DotNet.Runtime.7`を実行します。
ウェブページにアクセスすると、ブラウザが自動的にポップアップしてダウンロードされます。ファイルをデスクトップや他の見やすいフォルダに保存します。
ダウンロードが完了したら、dotnet-runtime-x.x.x-win-x64.exeという名前のファイルを開くと、インストールウィンドウがポップアップします。以下の図のように表示されます。
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide2.png)
インストールをクリックするだけです。
3. メインプログラムを開くための操作と実績エクスポートのオプション 3. メインプログラムを開くための操作と実績エクスポートのオプション
最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。 最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。
![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)
原神の起動が完了したら、ゲームに入ります。 原神の起動が完了したら、ゲームに入ります。
ゲームに入ると、ツールがどのツールにエクスポートするかを選択するように促します。以下の図のように表示されます。 ゲームに入ると、ツールがどのツールにエクスポートするかを選択するように促します。以下の図のように表示されます。
![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)
グローバルユーザーの場合、[3] Seelie.meまたは[4] csvファイルにエクスポートを選択する必要があります。 グローバルユーザーの場合、[3] Seelie.meまたは[4] csvファイルにエクスポートを選択する必要があります。
@@ -40,25 +26,25 @@
#### 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
この時点で、Yae Achievementは実績データがエクスポートされたことを通知します。Yae Achievement保存ディレクトリにexport-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを見つけてください。 この時点で、Yae Achievementは実績データがエクスポートされたことを通知します。Yae Achievement保存ディレクトリにexport-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを見つけてください。
![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)
次に、URLhttps://seelie.me/settings をクリックし、ウェブサイトにアクセスしてインポートアカウントを選択します。以下の図のように表示されます。 次に、URLhttps://seelie.me/settings をクリックし、ウェブサイトにアクセスしてインポートアカウントを選択します。以下の図のように表示されます。
![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)
インポートをクリックした後、export-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを選択します。以下の図のように表示されます。 インポートをクリックした後、export-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを選択します。以下の図のように表示されます。
![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)
以下の図のようなプロンプトが表示されたら、インポートプロセスは成功です。 以下の図のようなプロンプトが表示されたら、インポートプロセスは成功です。
![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)
この時点で、左側の列のAchievementsを選択して、インポートされた実績データを表示できます。 この時点で、左側の列のAchievementsを選択して、インポートされた実績データを表示できます。
@@ -68,7 +54,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://ja.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

@@ -0,0 +1,104 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace YaeAchievement.SourceGeneration;
[Generator(LanguageNames.CSharp)]
public sealed class MinHookAttachGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<AttachInfo>> provider = context.SyntaxProvider.CreateSyntaxProvider(Filter, Transform).Collect();
context.RegisterSourceOutput(provider, Generate);
}
private static bool Filter(SyntaxNode node, CancellationToken token)
{
return node is InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier.Text: "MinHook" },
Name.Identifier.Text: "Attach"
}
};
}
private static AttachInfo Transform(GeneratorSyntaxContext context, CancellationToken token)
{
InvocationExpressionSyntax invocation = (InvocationExpressionSyntax)context.Node;
SeparatedSyntaxList<ArgumentSyntax> args = invocation.ArgumentList.Arguments;
if (args.Count is not 3)
{
return null;
}
string type = context.SemanticModel.GetTypeInfo(args[0].Expression).Type?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
if (string.IsNullOrEmpty(type))
{
return null;
}
return new()
{
MinimallyQualifiedType = type,
};
}
private static void Generate(SourceProductionContext context, ImmutableArray<AttachInfo> infoArray)
{
CompilationUnitSyntax unit = CompilationUnit()
.WithMembers(List<MemberDeclarationSyntax>(
[
FileScopedNamespaceDeclaration(ParseName("Yae.Utilities")),
ClassDeclaration("MinHook")
.WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword)))
.WithMembers(List(GenerateMethods(infoArray)))
]));
context.AddSource("MinHook.Attach.g.cs", unit.NormalizeWhitespace().ToFullString());
}
private static IEnumerable<MemberDeclarationSyntax> GenerateMethods(ImmutableArray<AttachInfo> infoArray)
{
foreach (AttachInfo info in infoArray)
{
TypeSyntax type = ParseTypeName(info.MinimallyQualifiedType);
yield return MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("Attach"))
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.UnsafeKeyword)))
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(Identifier("origin")).WithType(type),
Parameter(Identifier("handler")).WithType(type),
Parameter(Identifier("trampoline")).WithType(type).WithModifiers(TokenList(Token(SyntaxKind.OutKeyword)))
])))
.WithBody(Block(List<StatementSyntax>(
[
ExpressionStatement(InvocationExpression(IdentifierName("Attach"))
.WithArgumentList(ArgumentList(SeparatedList(
[
Argument(CastExpression(IdentifierName("nint"), IdentifierName("origin"))),
Argument(CastExpression(IdentifierName("nint"), IdentifierName("handler"))),
Argument(DeclarationExpression(IdentifierName("nint"), SingleVariableDesignation(Identifier("trampoline1"))))
.WithRefKindKeyword(Token(SyntaxKind.OutKeyword))
])))),
ExpressionStatement(AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
IdentifierName("trampoline"),
CastExpression(type, IdentifierName("trampoline1"))))
])));
}
}
private record AttachInfo
{
public required string MinimallyQualifiedType { get; init; }
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

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

5
YaeAchievement.slnx Normal file
View File

@@ -0,0 +1,5 @@
<Solution>
<Project Path="YaeAchievement.SourceGeneration/YaeAchievement.SourceGeneration.csproj" />
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" />
<Project Path="YaeAchievement\YaeAchievement.csproj" />
</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": false
} }

View File

@@ -1,21 +1,30 @@
CloseClipboard // kernel32
CreateProcess
CreateRemoteThread
EmptyClipboard
GetConsoleMode
GetDC
GetDeviceCaps
GetModuleHandle
GetProcAddress
GetStdHandle
GlobalLock GlobalLock
OpenProcess
GetStdHandle
GlobalUnlock GlobalUnlock
OpenClipboard
ResumeThread ResumeThread
SetClipboardData Module32Next
Module32First
CreateProcess
LoadLibraryEx
VirtualFreeEx
VirtualAllocEx
GetProcAddress
GetConsoleMode
SetConsoleMode SetConsoleMode
TerminateProcess TerminateProcess
VirtualAllocEx CreateRemoteThread
VirtualFreeEx WriteProcessMemory
WaitForSingleObject WaitForSingleObject
WriteProcessMemory GetCurrentConsoleFontEx
CreateToolhelp32Snapshot
// psapi
GetModuleFileNameEx
// user32
OpenClipboard
CloseClipboard
EmptyClipboard
SetClipboardData

View File

@@ -0,0 +1,66 @@
<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.7.2</FileVersion>
<AssemblyVersion>5.7.2</AssemblyVersion>
<ApplicationIcon>res\icon.ico</ApplicationIcon>
<ApplicationManifest>res\app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<OptimizationPreference>Size</OptimizationPreference>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- [Update to 3.31.0 breaks AOT build](https://github.com/protocolbuffers/protobuf/issues/21824) -->
<PackageReference Include="Google.Protobuf" Version="3.30.2"/>
<PackageReference Include="Grpc.Tools" Version="2.72.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Sentry" Version="5.13.0"/>
<PackageReference Include="Spectre.Console" Version="0.50.1-preview.0.22"/>
<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

@@ -9,8 +9,8 @@
namespace YaeAchievement.res { namespace YaeAchievement.res {
using System; using System;
/// <summary> /// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc. /// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary> /// </summary>
@@ -22,15 +22,15 @@ namespace YaeAchievement.res {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class App { internal class App {
private static global::System.Resources.ResourceManager resourceMan; private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture; private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal App() { internal App() {
} }
/// <summary> /// <summary>
/// Returns the cached ResourceManager instance used by this class. /// Returns the cached ResourceManager instance used by this class.
/// </summary> /// </summary>
@@ -44,7 +44,7 @@ namespace YaeAchievement.res {
return resourceMan; return resourceMan;
} }
} }
/// <summary> /// <summary>
/// Overrides the current thread's CurrentUICulture property for all /// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class. /// resource lookups using this strongly typed resource class.
@@ -58,7 +58,7 @@ namespace YaeAchievement.res {
resourceCulture = value; resourceCulture = value;
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to all achievement. /// Looks up a localized string similar to all achievement.
/// </summary> /// </summary>
@@ -67,7 +67,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("AllAchievement", resourceCulture); return ResourceManager.GetString("AllAchievement", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Please close another instance.. /// Looks up a localized string similar to Please close another instance..
/// </summary> /// </summary>
@@ -76,7 +76,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("AnotherInstance", resourceCulture); return ResourceManager.GetString("AnotherInstance", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to YaeAchievement ({0}). /// Looks up a localized string similar to YaeAchievement ({0}).
/// </summary> /// </summary>
@@ -85,16 +85,34 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("AppBanner", resourceCulture); return ResourceManager.GetString("AppBanner", 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 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>
/// Looks up a localized string similar to Please launch GenshinImpact to continue..
/// </summary> /// </summary>
internal static string ConfigNeedStartGenshin { internal static string ConfigNeedStartGenshin {
get { get {
return ResourceManager.GetString("ConfigNeedStartGenshin", resourceCulture); return ResourceManager.GetString("ConfigNeedStartGenshin", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Download: {0}. /// Looks up a localized string similar to Download: {0}.
/// </summary> /// </summary>
@@ -103,7 +121,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("DownloadLink", resourceCulture); return ResourceManager.GetString("DownloadLink", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Network error ({0}: {1}). /// Looks up a localized string similar to Network error ({0}: {1}).
/// </summary> /// </summary>
@@ -112,34 +130,106 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExceptionNetwork", resourceCulture); return ResourceManager.GetString("ExceptionNetwork", resourceCulture);
} }
} }
/// <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 {
return ResourceManager.GetString("ExportChoose", resourceCulture); return ResourceManager.GetString("ExportChoose", 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 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>
/// Looks up a localized string similar to Fail, please contact developer to get help information (CG_{0}).
/// </summary> /// </summary>
internal static string ExportToCocogoatFail { internal static string ExportToCocogoatFail {
get { get {
return ResourceManager.GetString("ExportToCocogoatFail", resourceCulture); return ResourceManager.GetString("ExportToCocogoatFail", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Successfully exported to cocogoat.. /// Looks up a localized string similar to Successfully exported to cocogoat..
/// </summary> /// </summary>
@@ -148,7 +238,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToCocogoatSuccess", resourceCulture); return ResourceManager.GetString("ExportToCocogoatSuccess", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Successfully exported to {0}. /// Looks up a localized string similar to Successfully exported to {0}.
/// </summary> /// </summary>
@@ -157,7 +247,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToFileSuccess", resourceCulture); return ResourceManager.GetString("ExportToFileSuccess", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Please update Snap Hutao and retry.. /// Looks up a localized string similar to Please update Snap Hutao and retry..
/// </summary> /// </summary>
@@ -166,7 +256,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToSnapGenshinNeedUpdate", resourceCulture); return ResourceManager.GetString("ExportToSnapGenshinNeedUpdate", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Successfully exported to Snap Hutao.. /// Looks up a localized string similar to Successfully exported to Snap Hutao..
/// </summary> /// </summary>
@@ -175,7 +265,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToSnapGenshinSuccess", resourceCulture); return ResourceManager.GetString("ExportToSnapGenshinSuccess", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Please launch/update Teyvat Guide and retry.. /// Looks up a localized string similar to Please launch/update Teyvat Guide and retry..
/// </summary> /// </summary>
@@ -184,7 +274,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToTauriFail", resourceCulture); return ResourceManager.GetString("ExportToTauriFail", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Successfully exported to Teyvat Guide.. /// Looks up a localized string similar to Successfully exported to Teyvat Guide..
/// </summary> /// </summary>
@@ -193,7 +283,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToTauriSuccess", resourceCulture); return ResourceManager.GetString("ExportToTauriSuccess", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to {0}. /// Looks up a localized string similar to {0}.
/// </summary> /// </summary>
@@ -202,7 +292,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToWxApp1Success", resourceCulture); return ResourceManager.GetString("ExportToWxApp1Success", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Please update xunkong and retry.. /// Looks up a localized string similar to Please update xunkong and retry..
/// </summary> /// </summary>
@@ -211,7 +301,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToXunkongNeedUpdate", resourceCulture); return ResourceManager.GetString("ExportToXunkongNeedUpdate", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Successfully exported to xunkong.. /// Looks up a localized string similar to Successfully exported to xunkong..
/// </summary> /// </summary>
@@ -220,7 +310,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToXunkongSuccess", resourceCulture); return ResourceManager.GetString("ExportToXunkongSuccess", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Game process start ({0}). /// Looks up a localized string similar to Game process start ({0}).
/// </summary> /// </summary>
@@ -229,7 +319,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GameLoading", resourceCulture); return ResourceManager.GetString("GameLoading", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Game exited.. /// Looks up a localized string similar to Game exited..
/// </summary> /// </summary>
@@ -238,7 +328,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GameProcessExit", resourceCulture); return ResourceManager.GetString("GameProcessExit", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Please update genshin and retry.. /// Looks up a localized string similar to Please update genshin and retry..
/// </summary> /// </summary>
@@ -247,7 +337,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GenshinHashError", resourceCulture); return ResourceManager.GetString("GenshinHashError", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Please close game before run this application. ({0}). /// Looks up a localized string similar to Please close game before run this application. ({0}).
/// </summary> /// </summary>
@@ -256,7 +346,16 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GenshinIsRunning", resourceCulture); return ResourceManager.GetString("GenshinIsRunning", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Load failed. Please retry. ({0} {1} {2}).
/// </summary>
internal static string LoadLibraryFail {
get {
return ResourceManager.GetString("LoadLibraryFail", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Network error:. /// Looks up a localized string similar to Network error:.
/// </summary> /// </summary>
@@ -265,7 +364,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("NetworkError", resourceCulture); return ResourceManager.GetString("NetworkError", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to No write permission on {0}.. /// Looks up a localized string similar to No write permission on {0}..
/// </summary> /// </summary>
@@ -274,7 +373,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("NoWritePermission", resourceCulture); return ResourceManager.GetString("NoWritePermission", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Press any key to exit.. /// Looks up a localized string similar to Press any key to exit..
/// </summary> /// </summary>
@@ -283,7 +382,43 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("PressKeyToExit", resourceCulture); return ResourceManager.GetString("PressKeyToExit", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Use the keyboard arrow keys to move the cursor and the Enter key to select.
/// </summary>
internal static string SelectionPromptCompatAnsiTip {
get {
return ResourceManager.GetString("SelectionPromptCompatAnsiTip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose an option:.
/// </summary>
internal static string SelectionPromptCompatChooseOne {
get {
return ResourceManager.GetString("SelectionPromptCompatChooseOne", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please enter a number between 0 and {0}.
/// </summary>
internal static string SelectionPromptCompatInvalidChoice {
get {
return ResourceManager.GetString("SelectionPromptCompatInvalidChoice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Type a number and press Enter to select.
/// </summary>
internal static string SelectionPromptCompatNonAnsiTip {
get {
return ResourceManager.GetString("SelectionPromptCompatNonAnsiTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Reward not taken. /// Looks up a localized string similar to Reward not taken.
/// </summary> /// </summary>
@@ -292,7 +427,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusFinished", resourceCulture); return ResourceManager.GetString("StatusFinished", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Invalid. /// Looks up a localized string similar to Invalid.
/// </summary> /// </summary>
@@ -301,7 +436,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusInvalid", resourceCulture); return ResourceManager.GetString("StatusInvalid", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Finished. /// Looks up a localized string similar to Finished.
/// </summary> /// </summary>
@@ -310,7 +445,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusRewardTaken", resourceCulture); return ResourceManager.GetString("StatusRewardTaken", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Unfinished. /// Looks up a localized string similar to Unfinished.
/// </summary> /// </summary>
@@ -319,9 +454,27 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusUnfinished", resourceCulture); return ResourceManager.GetString("StatusUnfinished", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Description: /// Looks up a localized string similar to An error occurred while reading the data. Please try again..
/// </summary>
internal static string StreamReadDataFail {
get {
return ResourceManager.GetString("StreamReadDataFail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Checking update....
/// </summary>
internal static string UpdateChecking {
get {
return ResourceManager.GetString("UpdateChecking", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Description:
///{0}. ///{0}.
/// </summary> /// </summary>
internal static string UpdateDescription { internal static string UpdateDescription {
@@ -329,7 +482,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UpdateDescription", resourceCulture); return ResourceManager.GetString("UpdateDescription", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Downloading update package.... /// Looks up a localized string similar to Downloading update package....
/// </summary> /// </summary>
@@ -338,7 +491,16 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UpdateDownloading", resourceCulture); return ResourceManager.GetString("UpdateDownloading", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to The process cannot access the file &apos;{0}&apos; because it is being used by another process. Please restart your computer and try again..
/// </summary>
internal static string UpdateFileShareViolation {
get {
return ResourceManager.GetString("UpdateFileShareViolation", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Has update: {0} =&gt; {1}. /// Looks up a localized string similar to Has update: {0} =&gt; {1}.
/// </summary> /// </summary>
@@ -347,17 +509,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UpdateNewVersion", resourceCulture); return ResourceManager.GetString("UpdateNewVersion", resourceCulture);
} }
} }
/// <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>
@@ -366,16 +518,16 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UploadError", resourceCulture); return ResourceManager.GetString("UploadError", resourceCulture);
} }
} }
/// <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 {
return ResourceManager.GetString("UsePreviousData", resourceCulture); return ResourceManager.GetString("UsePreviousData", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Downloading Visual C++ Redistributable.... /// Looks up a localized string similar to Downloading Visual C++ Redistributable....
/// </summary> /// </summary>
@@ -384,7 +536,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("VcRuntimeDownload", resourceCulture); return ResourceManager.GetString("VcRuntimeDownload", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Installing Visual C++ Redistributable.... /// Looks up a localized string similar to Installing Visual C++ Redistributable....
/// </summary> /// </summary>
@@ -393,5 +545,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

@@ -19,22 +19,13 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ExportToCocogoatFail" xml:space="preserve"> <data name="ExportToCocogoatFail" xml:space="preserve">
<value>Fail, please contact developer to get help information</value> <value>Fail, please contact developer to get help information (CG_{0})</value>
</data> </data>
<data name="AllAchievement" xml:space="preserve"> <data name="AllAchievement" xml:space="preserve">
<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>
@@ -70,7 +61,7 @@ Input a number (0-8): </value>
<value>Reward not taken</value> <value>Reward not taken</value>
</data> </data>
<data name="ConfigNeedStartGenshin" xml:space="preserve"> <data name="ConfigNeedStartGenshin" xml:space="preserve">
<value>You need to login genshin impact before exporting.</value> <value>Please launch GenshinImpact to continue.</value>
</data> </data>
<data name="DownloadLink" xml:space="preserve"> <data name="DownloadLink" xml:space="preserve">
<value>Download: {0}</value> <value>Download: {0}</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,64 @@ 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>
<data name="SelectionPromptCompatChooseOne" xml:space="preserve">
<value>Choose an option:</value>
</data>
<data name="SelectionPromptCompatAnsiTip" xml:space="preserve">
<value>Use the keyboard arrow keys to move the cursor and the Enter key to select</value>
</data>
<data name="SelectionPromptCompatNonAnsiTip" xml:space="preserve">
<value>Type a number and press Enter to select</value>
</data>
<data name="SelectionPromptCompatInvalidChoice" xml:space="preserve">
<value>Please enter a number between 0 and {0}</value>
</data>
<data name="StreamReadDataFail" xml:space="preserve">
<value>An error occurred while reading the data. Please try again.</value>
</data>
<data name="UpdateFileShareViolation" xml:space="preserve">
<value>The process cannot access the file '{0}' because it is being used by another process. Please restart your computer and try again.</value>
</data>
<data name="LoadLibraryFail" xml:space="preserve">
<value>Load failed. Please retry. ({0} {1} {2})</value>
</data>
</root> </root>

View File

@@ -12,23 +12,13 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ExportToCocogoatFail" xml:space="preserve"> <data name="ExportToCocogoatFail" xml:space="preserve">
<value>导出失败, 请联系开发者以获取帮助</value> <value>导出失败, 请联系开发者以获取帮助CG_{0}</value>
</data> </data>
<data name="AllAchievement" xml:space="preserve"> <data name="AllAchievement" xml:space="preserve">
<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>
@@ -64,7 +54,7 @@
<value>已完成</value> <value>已完成</value>
</data> </data>
<data name="ConfigNeedStartGenshin" xml:space="preserve"> <data name="ConfigNeedStartGenshin" xml:space="preserve">
<value>在导出前你需要先完成一次登入流程.</value> <value>请打开 原神 后继续操作</value>
</data> </data>
<data name="DownloadLink" xml:space="preserve"> <data name="DownloadLink" xml:space="preserve">
<value>下载地址: {0}</value> <value>下载地址: {0}</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,64 @@
<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>
<data name="SelectionPromptCompatChooseOne" xml:space="preserve">
<value>选择一个选项:</value>
</data>
<data name="SelectionPromptCompatAnsiTip" xml:space="preserve">
<value>键盘上下键移动光标,键盘回车键选择</value>
</data>
<data name="SelectionPromptCompatNonAnsiTip" xml:space="preserve">
<value>输入数字并回车以选择选项</value>
</data>
<data name="SelectionPromptCompatInvalidChoice" xml:space="preserve">
<value>请输入 0 到 {0} 之间的数字</value>
</data>
<data name="StreamReadDataFail" xml:space="preserve">
<value>读取数据时发生错误,请重试</value>
</data>
<data name="UpdateFileShareViolation" xml:space="preserve">
<value>文件 {0} 被其它程序占用,请 重启电脑 或 解除文件占用 后重试。</value>
</data>
<data name="LoadLibraryFail" xml:space="preserve">
<value>加载失败,请重试({0} {1} {2}</value>
</data>
</root> </root>

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -0,0 +1,42 @@
syntax = "proto3";
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 {
uint32 pre = 1;
uint32 group = 2;
string name = 3;
string description = 4;
}
message MethodRvaConfig {
uint32 do_cmd = 1;
uint32 to_uint16 = 2;
uint32 update_normal_prop = 3;
uint32 new_string = 4;
uint32 find_game_object = 5;
uint32 event_system_update = 6;
uint32 simulate_pointer_click = 7;
}
message NativeLibConfig {
uint32 store_cmd_id = 1;
uint32 achievement_cmd_id = 2;
map<uint32, MethodRvaConfig> method_rva = 10;
}
message AchievementInfo {
string version = 1;
map<uint32, string> group = 2;
map<uint32, AchievementItem> items = 3;
AchievementProtoFieldInfo pb_info = 4;
NativeLibConfig native_config = 5;
}

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,20 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message CdnFileInfo {
uint32 size = 1;
uint32 hash = 2;
repeated string urls = 4;
}
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;
map<string, CdnFileInfo> cdn_files = 8;
}

View File

@@ -0,0 +1,90 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.RegularExpressions;
using Spectre.Console;
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;
} else if (TryReadGamePathFromCache(out var cachedPath)) {
GamePath = cachedPath;
} else if (TryReadGamePathFromUnityLog(out var loggedPath)) {
GamePath = loggedPath;
} else {
GamePath = ReadGamePathFromProcess();
}
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{Utils.GetGameHash(GamePath)}"));
SentrySdk.AddBreadcrumb(GamePath.EndsWith("YuanShen.exe") ? "CN" : "OS", "GamePath");
return;
static bool TryReadGamePathFromCache([NotNullWhen(true)] out string? path) {
path = null;
try {
if (!CacheFile.TryRead("genshin_impact_game_path_v2", out var cacheFile)) {
return false;
}
var cacheData = cacheFile.Content.ToStringUtf8().Split("\u1145");
if (Utils.GetGameHash(cacheData[0]) != uint.Parse(cacheData[1])) {
return false;
}
path = cacheData[0];
return true;
} catch (Exception) {
return false;
}
}
static bool TryReadGamePathFromUnityLog([NotNullWhen(true)] out string? path) {
path = null;
try {
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) {
return false;
}
return (path = GetGamePathFromLogFile(logPath) ?? GetGamePathFromLogFile($"{logPath}.last")) != null;
} catch (Exception) {
return false;
}
}
static string ReadGamePathFromProcess() {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).Start(App.ConfigNeedStartGenshin, _ => {
Process? proc;
while ((proc = Utils.GetGameProcess()) == null) {
Thread.Sleep(250);
}
var fileName = proc.GetFileName()!;
proc.Kill();
return fileName;
});
}
}
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");
var fullPath = Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
return File.Exists(fullPath) ? fullPath : null;
}
[GeneratedRegex(@"(?m).:(?:\\|/).+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
private static partial Regex GamePathRegex();
}

View File

@@ -0,0 +1,237 @@
using System.ComponentModel;
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.Utilities;
// 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 SelectionPromptCompat<string>().Title(App.ExportChoose).AddChoices(targets.Keys);
action = targets[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, response.StatusCode);
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 sealed 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 sealed 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 sealed record CocogoatResponse(string Key);
[JsonSerializable(typeof(CocogoatResponse))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public sealed partial class CocogoatResponseContext : JsonSerializerContext;

View File

@@ -1,16 +1,14 @@
using System.Reflection; global using System.Diagnostics;
global using YaeAchievement.res;
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!;
@@ -20,14 +18,18 @@ public static class GlobalVars {
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 = 51; public const uint AppVersionCode = 242;
public const string AppVersionName = "4.1"; public const string AppVersionName = "5.7.2";
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 sealed 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 sealed 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 sealed class SeelieRoot {
public sealed 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 sealed 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 sealed class UApplicationInfo {
public string ExportApp => "YaeAchievement";
public string ExportAppVersion => GlobalVars.AppVersionName;
public long ExportTimestamp => DateTimeOffset.Now.ToUnixTimeSeconds();
public string UIAFVersion => "v1.1";
}
public sealed class UAchievementInfo {
public uint Id { get; set; }
public uint Status { get; set; }
public uint Current { get; set; }
public uint Timestamp { get; set; }
}
public sealed 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 sealed partial class UIAFSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(Outputs.UIAFRoot.FromNotify(ntf), Default.UIAFRoot);
}
}

View File

@@ -0,0 +1,151 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Spectre.Console;
using YaeAchievement.Utilities;
namespace YaeAchievement.Parsers;
public enum AchievementStatus {
Invalid,
Unfinished,
Finished,
RewardTaken,
}
public sealed 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 sealed 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();
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 is { Count: > 2 }) { // at least 3 fields
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.All(CheckKnownFieldIdIsValid)) {
var info = GlobalVars.AchievementInfo.PbInfo;
iId = info.Id;
tId = info.FinishTimestamp;
sId = info.Status;
totalId = info.TotalProgress;
currentId = info.CurrentProgress;
} else if (data.Count > 20) {
(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 {
AnsiConsole.WriteLine(App.WaitMetadataUpdate);
Environment.Exit(0);
return null!;
}
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()
};
// ReSharper disable once ConvertIfStatementToSwitchStatement
static bool CheckKnownFieldIdIsValid(Dictionary<uint, uint> data) {
var info = GlobalVars.AchievementInfo;
var status = data.GetValueOrDefault(info.PbInfo.Status, 114514u);
if (status is 0 or > 3) {
return false;
}
if (status > 1 && data.GetValueOrDefault(info.PbInfo.FinishTimestamp) < 1600114514) { // 2020-09-15 04:15:14
return false;
}
return info.Items.ContainsKey(data.GetValueOrDefault(info.PbInfo.Id));
}
}
}
[JsonSerializable(typeof(AchievementAllDataNotify))]
[JsonSourceGenerationOptions(
WriteIndented = true,
GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)]
public sealed 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 sealed 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();
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,95 @@
using System.Runtime.CompilerServices;
using System.Text;
using Spectre.Console;
using YaeAchievement.Parsers;
using YaeAchievement.Utilities;
using static YaeAchievement.Utils;
namespace YaeAchievement;
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);
}
SentrySdk.Init(options => {
options.Dsn = "https://92f11b64b0ef52cabc94f21df0428f5b@sentry.snapgenshin.com/9";
#if DEBUG
options.Debug = true;
#endif
options.TracesSampleRate = 1.0;
options.AutoSessionTracking = true;
options.SetBeforeSend(static e => {
e.Release = GlobalVars.AppVersionName;
return e;
});
options.SetBeforeSendTransaction(static e => {
e.Release = GlobalVars.AppVersionName;
return e;
});
options.CacheDirectoryPath = GlobalVars.DataPath;
});
InstallExitHook();
InstallExceptionHook();
if (GetGameProcess() != null) {
AnsiConsole.WriteLine(App.GenshinIsRunning, 0);
Environment.Exit(-1);
}
await CheckUpdate(ToBooleanOrDefault(args.ElementAtOrDefault(2)));
AppConfig.Load(args.ElementAtOrDefault(0) ?? "auto");
Export.ExportTo = ToIntOrDefault(args.ElementAtOrDefault(1), 114514);
AchievementAllDataNotify? data = null;
try {
if (CacheFile.TryRead("achievement_data", out var cache)) {
data = AchievementAllDataNotify.ParseFrom(cache.Content.ToByteArray());
}
} catch (Exception) { /* ignored */ }
if (data != null && CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow) {
var prompt = new SelectionPromptCompat<string>()
.Title(App.UsePreviousData)
.AddChoices(App.CommonYes, App.CommonNo);
if (prompt.Prompt() == App.CommonYes) {
Export.Choose(data);
return;
}
}
StartAndWaitResult(AppConfig.GamePath, new Dictionary<int, Func<BinaryReader, bool>> {
{ 1, AchievementAllDataNotify.OnReceive },
{ 2, PlayerStoreNotify.OnReceive },
{ 3, 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;
FixTerminalFont();
}
}

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,26 @@
namespace YaeAchievement.Utilities;
// CRC-32-IEEE 802.3
public static class Crc32 {
private const uint Polynomial = 0xEDB88320;
private static readonly uint[] Crc32Table = new uint[256];
static Crc32() {
for (uint i = 0; i < Crc32Table.Length; i++) {
var v = i;
for (var j = 0; j < 8; j++) {
v = (v >> 1) ^ ((v & 1) * Polynomial);
}
Crc32Table[i] = v;
}
}
public static uint Compute(Span<byte> buf) {
var checksum = 0xFFFFFFFF;
foreach (var b in buf) {
checksum = (checksum >> 8) ^ Crc32Table[(b ^ checksum) & 0xFF];
}
return ~checksum;
}
}

View File

@@ -0,0 +1,31 @@
using System.ComponentModel;
namespace System.Collections.Generic {
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class CollectionExtensions {
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;
}
}
}
namespace System.Linq {
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class EnumerableExtensions {
public static IEnumerable<IGrouping<TKey, TKey>> GroupKeys<TKey, TValue>(
this IEnumerable<Dictionary<TKey, TValue>> source,
Func<TValue, bool> condition
) where TKey : notnull => source
.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key))
.GroupBy(x => x);
}
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel;
using Windows.Win32;
using Windows.Win32.Foundation;
using static Windows.Win32.System.Threading.PROCESS_ACCESS_RIGHTS;
// ReSharper disable CheckNamespace
namespace System.Diagnostics;
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class ProcessExtensions {
public static unsafe string? GetFileName(this Process process) {
using var hProc = Native.OpenProcess_SafeHandle(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, (uint) process.Id);
if (hProc.IsInvalid) {
return null;
}
var sProcPath = stackalloc char[32767];
return Native.GetModuleFileNameEx((HANDLE) hProc.DangerousGetHandle(), HMODULE.Null, sProcPath, 32767) == 0
? null
: new string(sProcPath);
}
}

View File

@@ -0,0 +1,38 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Spectre.Console;
// ReSharper disable CheckNamespace
namespace Google.Protobuf;
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class BinaryReaderExtensions {
public static byte[] ReadBytes(this BinaryReader reader) {
try {
var length = reader.ReadInt32();
if (length is < 0 or > 114514 * 2) {
throw new ArgumentException(nameof(length));
}
return reader.ReadBytes(length);
} catch (Exception e) when (e is IOException or ArgumentException) {
AnsiConsole.WriteLine(App.StreamReadDataFail);
Environment.Exit(-1);
throw new UnreachableException();
}
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
internal 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,127 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using Windows.Win32.System.LibraryLoader;
using Windows.Win32.System.Threading;
using Spectre.Console;
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;
internal sealed 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)) {
var argumentData = new Dictionary<string, object> {
{ "path", path },
{ "workdir", wd },
{ "file_exists", File.Exists(path) },
};
throw new ApplicationException($"CreateProcess fail: {Marshal.GetLastPInvokeErrorMessage()}") {
Data = {
{ "sentry:context:Arguments", argumentData }
}
};
}
Id = pi.dwProcessId;
Handle = pi.hProcess;
MainThreadHandle = pi.hThread;
Task.Run(() => {
Native.WaitForSingleObject(Handle, 0xFFFFFFFF); // INFINITE
OnExit?.Invoke();
}).ContinueWith(task => { if (task.IsFaulted) Utils.OnUnhandledException(task.Exception!); });
}
public void LoadLibrary(string libPath) {
try {
var hKrnl32 = NativeLibrary.Load("kernel32");
var mLoadLibraryW = NativeLibrary.GetExport(hKrnl32, "LoadLibraryW");
var libPathLen = (uint) libPath.Length * sizeof(char);
var lpLibPath = Native.VirtualAllocEx(Handle, null, libPathLen + 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (lpLibPath == null) {
throw new Win32Exception { Data = { { "api", "VirtualAllocEx" } } };
}
fixed (void* lpBuffer = libPath) {
if (!Native.WriteProcessMemory(Handle, lpLibPath, lpBuffer, libPathLen)) {
throw new Win32Exception { Data = { { "api", "WriteProcessMemory" } } };
}
}
var lpStartAddress = (delegate*unmanaged[Stdcall]<void*, uint>) mLoadLibraryW; // THREAD_START_ROUTINE
var hThread = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress, lpLibPath, 0);
if (hThread.IsNull) {
throw new Win32Exception { Data = { { "api", "CreateRemoteThread" } } };
}
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
}
// Get lib base address in target process
byte* baseAddress = null;
using (var hSnap = Native.CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, Id)) {
if (hSnap.IsInvalid) {
throw new Win32Exception { Data = { { "api", "CreateToolhelp32Snapshot" } } };
}
var moduleEntry = new MODULEENTRY32 {
dwSize = (uint) sizeof(MODULEENTRY32)
};
if (Native.Module32First(hSnap, ref moduleEntry)) {
do {
if (new string((sbyte*) &moduleEntry.szExePath._0) == libPath) {
baseAddress = moduleEntry.modBaseAddr;
break;
}
} while (Native.Module32Next(hSnap, ref moduleEntry));
}
}
if (baseAddress == null) {
throw new InvalidOperationException("No matching module found in target process.");
}
//
using var libHandle = Native.LoadLibraryEx(libPath, LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES);
if (libHandle.IsInvalid) {
throw new Win32Exception { Data = { { "api", "LoadLibraryEx" } } };
}
var libMainProc = Native.GetProcAddress(libHandle, "YaeMain");
if (libMainProc.IsNull) {
throw new Win32Exception { Data = { { "api", "GetProcAddress" } } };
}
var libMainProcRVA = libMainProc.Value - libHandle.DangerousGetHandle();
var lpStartAddress2 = (delegate*unmanaged[Stdcall]<void*, uint>) (baseAddress + libMainProcRVA); // THREAD_START_ROUTINE
//
var hThread2 = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress2, null, 0);
if (hThread2.IsNull) {
throw new Win32Exception { Data = { { "api", "CreateRemoteThread2" } } };
}
Native.CloseHandle(hThread2);
Native.CloseHandle(hThread);
} catch (Win32Exception e) {
_ = Terminate(0);
AnsiConsole.WriteLine(App.LoadLibraryFail, e.Data["api"]!, e.NativeErrorCode, e.Message);
Environment.Exit(-1);
}
}
public bool ResumeMainThread() => Native.ResumeThread(MainThreadHandle) != 0xFFFFFFFF;
public bool Terminate(uint exitCode) => Native.TerminateProcess(Handle, exitCode);
}

View File

@@ -0,0 +1,47 @@
using Spectre.Console;
namespace YaeAchievement.Utilities;
public sealed class SelectionPromptCompat<T> where T : notnull {
private readonly List<T> _choices = [];
private readonly SelectionPrompt<T> _prompt = new ();
public SelectionPromptCompat<T> Title(string? title) {
_prompt.Title = title;
return this;
}
public SelectionPromptCompat<T> AddChoices(params IEnumerable<T> choices) {
foreach (var choice in choices) {
_prompt.AddChoice(choice);
_choices.Add(choice);
}
return this;
}
public T Prompt() {
if (AnsiConsole.Profile.Capabilities.Ansi) {
var title = _prompt.Title;
_prompt.Title += $" ({App.SelectionPromptCompatAnsiTip})";
var result = AnsiConsole.Prompt(_prompt);
_prompt.Title = title;
return result;
}
if (_prompt.Title != null) {
AnsiConsole.WriteLine(_prompt.Title + $" ({App.SelectionPromptCompatNonAnsiTip})");
}
for (var i = 0; i < _choices.Count; i++) {
var choice = _choices[i];
AnsiConsole.WriteLine($"[{i}] {choice}");
}
var choosePrompt = new TextPrompt<int>(App.SelectionPromptCompatChooseOne).Validate(i => {
if (i < 0 || i >= _choices.Count) {
return ValidationResult.Error(string.Format(App.SelectionPromptCompatInvalidChoice, _choices.Count - 1));
}
return ValidationResult.Success();
});
var resultIndex = AnsiConsole.Prompt(choosePrompt);
return _choices[resultIndex];
}
}

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

@@ -0,0 +1,355 @@
using System.ComponentModel;
using System.Globalization;
using System.IO.Compression;
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using Proto;
using Spectre.Console;
using YaeAchievement.Utilities;
namespace YaeAchievement;
public static class Utils {
public static HttpClient CHttpClient { get; } = new (new SentryHttpMessageHandler(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) {
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
SentrySdk.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction);
var cacheKey = useCache ? path : null;
//
if (_updateInfo?.CdnFiles.TryGetValue(path, out var cdnFile) == true) {
try {
var data = await cdnFile.Urls
.Select(url => GetFileFromCdn(url, cacheKey, cdnFile.Hash, cdnFile.Size))
.WhenFirstSuccessful()
.Unwrap();
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException or InvalidDataException) {}
}
//
try {
var data = await WhenFirstSuccessful([
GetFileReal($"https://rin.holohat.work/{path}", cacheKey),
GetFileReal($"https://ena-rin.holohat.work//{path}", cacheKey),
GetFileReal($"https://cn-cd-1259389942.file.myqcloud.com/{path}", cacheKey)
]).Unwrap();
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
AnsiConsole.WriteLine(App.NetworkError, e.Message);
}
transaction.Finish();
Environment.Exit(-1);
throw new UnreachableException();
static async Task<byte[]> GetFileFromCdn(string url, string? cacheKey, uint hash, uint size) {
var data = await GetFileReal(url, cacheKey);
if (data.Length != size || Crc32.Compute(data) != hash) {
throw new InvalidDataException();
}
if (data.Length > 44 && Unsafe.As<byte, uint>(ref data[0]) == 0x38464947) { // GIF8
var seed = Unsafe.As<byte, uint>(ref data[44]) ^ 0x01919810;
var hush = Unsafe.As<byte, uint>(ref data[48]) - 0x32123432; //          
var span = data.AsSpan()[52..];
Span<byte> xorTable = stackalloc byte[4096];
new Random((int) seed).NextBytes(xorTable);
for (var i = 0; i < span.Length; i++) {
span[i] ^= xorTable[i % 4096];
}
using var dataStream = new MemoryStream();
unsafe {
fixed (byte* p = span) {
var cmpStream = new UnmanagedMemoryStream(p, span.Length);
using var decompressor = new BrotliStream(cmpStream, CompressionMode.Decompress);
// ReSharper disable once MethodHasAsyncOverload
decompressor.CopyTo(dataStream);
}
}
data = dataStream.ToArray();
if (Crc32.Compute(data) != hush) {
throw new InvalidDataException();
}
}
return data;
}
static async Task<byte[]> GetFileReal(string url, string? cacheKey) {
using var reqwest = new HttpRequestMessage(HttpMethod.Get, url);
CacheItem? cache = null;
if (cacheKey != null && CacheFile.TryRead(cacheKey, 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 (cacheKey != null) {
var etag = response.Headers.ETag!.Tag;
CacheFile.Write(cacheKey, bytes, etag);
}
return bytes;
}
}
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;
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) {
try {
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
var versionInfo = _updateInfo = 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); // 要求重启电脑
}
} catch (IOException e) when ((uint) e.HResult == 0x80070020) { // ERROR_SHARING_VIOLATION
// IO_SharingViolation_File
// The process cannot access the file '{0}' because it is being used by another process.
AnsiConsole.WriteLine("文件 {0} 被其它程序占用,请 重启电脑 或 解除文件占用 后重试。", e.Message[36..^46]);
Environment.Exit(-1);
}
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static bool ShellOpen(string path, string args = "") {
try {
return new Process {
StartInfo = new ProcessStartInfo {
FileName = path,
UseShellExecute = true,
Arguments = args
}
}.Start();
} catch (Exception) {
return false;
}
}
internal static Process? GetGameProcess() => Process.GetProcessesByName("YuanShen")
.Concat(Process.GetProcessesByName("GenshinImpact"))
.FirstOrDefault(p => File.Exists($"{p.GetFileName()}/../HoYoKProtect.sys"));
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) => OnUnhandledException((Exception) e.ExceptionObject);
}
public static void OnUnhandledException(Exception ex) {
SentrySdk.CaptureException(ex);
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) {
var hash = GetGameHash(exePath);
var nativeConf = GlobalVars.AchievementInfo.NativeConfig;
if (!nativeConf.MethodRva.TryGetValue(hash, out var methodRva)) {
AnsiConsole.WriteLine($"No match config {exePath} {hash:X8}");
Environment.Exit(0);
return;
}
Task.Run(() => {
try {
using var stream = new NamedPipeServerStream(GlobalVars.PipeName);
using var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, true);
using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true);
stream.WaitForConnection();
int type;
while ((type = stream.ReadByte()) != -1) {
switch (type) {
case 0xFC:
writer.Write(nativeConf.AchievementCmdId);
writer.Write(nativeConf.StoreCmdId);
break;
case 0xFD:
writer.Write(methodRva.DoCmd);
writer.Write(methodRva.ToUint16);
writer.Write(methodRva.UpdateNormalProp);
writer.Write(methodRva.NewString);
writer.Write(methodRva.FindGameObject);
writer.Write(methodRva.EventSystemUpdate);
writer.Write(methodRva.SimulatePointerClick);
break;
case 0xFE:
_proc!.ResumeMainThread();
break;
case 0xFF:
writer.Write(true);
_isUnexpectedExit = false;
onFinish();
return;
}
if (handlers.TryGetValue(type, out var handler)) {
if (handler(reader)) {
handlers.Remove(type);
}
}
}
} catch (IOException e) when (e.Message == "Pipe is broken.") { } // SR.IO_PipeBroken
}).ContinueWith(task => { if (task.IsFaulted) OnUnhandledException(task.Exception!); });
_proc = new GameProcess(exePath);
_proc.LoadLibrary(GlobalVars.LibFilePath);
_proc.OnExit += () => {
if (_isUnexpectedExit) {
_proc = null;
AnsiConsole.WriteLine(App.GameProcessExit);
Environment.Exit(114514);
}
};
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
}
public static uint GetGameHash(string exePath) {
try {
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(exePath);
_ = stream.ReadAtLeast(buffer, 0x10000, false);
return Crc32.Compute(buffer);
} catch (IOException) {
return 0xFFFFFFFF;
}
}
internal 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);
}
internal static unsafe void FixTerminalFont() {
if (!CultureInfo.CurrentCulture.Name.StartsWith("zh")) {
return;
}
var handle = Native.GetStdHandle(STD_HANDLE.STD_OUTPUT_HANDLE);
var fontInfo = new CONSOLE_FONT_INFOEX {
cbSize = (uint) sizeof(CONSOLE_FONT_INFOEX)
};
if (!Native.GetCurrentConsoleFontEx(handle, false, &fontInfo)) {
return;
}
if (fontInfo.FaceName.ToString() == "Terminal") { // 点阵字体
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); // todo: use better way like auto set console font etc.
}
}
// https://stackoverflow.com/a/76953892
private static async Task<Task<TResult>> WhenFirstSuccessful<TResult>(this IEnumerable<Task<TResult>> tasks) {
var cts = new CancellationTokenSource();
Task<TResult>? selectedTask = null;
var continuations = tasks
.TakeWhile(_ => !cts.IsCancellationRequested)
.Select(task => {
return task.ContinueWith(t => {
if (t.IsCompletedSuccessfully) {
if (Interlocked.CompareExchange(ref selectedTask, t, null) is null) {
cts.Cancel();
}
}
}, cts.Token, TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
});
var whenAll = Task.WhenAll(continuations);
try {
await whenAll.ConfigureAwait(false);
} catch when (whenAll.IsCanceled) { /* ignore */ }
return selectedTask!;
}
}

View File

@@ -0,0 +1,71 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<RootNamespace>Yae</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFramework>net9.0-windows</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<PublishAot>true</PublishAot>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<InvariantGlobalization>true</InvariantGlobalization>
<OptimizationPreference>Speed</OptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<DirectPInvoke Include="NTDLL" />
<DirectPInvoke Include="USER32" />
<DirectPInvoke Include="KERNEL32" />
<DirectPInvoke Include="libMinHook.x64" />
<NativeLibrary Include="lib\libMinHook.x64.lib" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute" />
</ItemGroup>
<ItemGroup>
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.CodeAnalysis" />
</ItemGroup>
<PropertyGroup>
<PackageId>Yae.Lib</PackageId>
<Version>5.4.3</Version>
<Authors>HoloHat</Authors>
<DevelopmentDependency>true</DevelopmentDependency>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<RepositoryUrl>https://github.com/HolographicHat/Yae</RepositoryUrl>
<Description>Yae Lib</Description>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<ItemGroup>
<None Include="$(PublishDir)\$(TargetName)$(NativeBinaryExt)" Pack="true" PackagePath="runtimes\win-x64\native" Visible="false" />
</ItemGroup>
<ItemGroup>
<ProjectReference OutputItemType="Analyzer" Include="..\YaeAchievement.SourceGeneration\YaeAchievement.SourceGeneration.csproj">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</ProjectReference>
</ItemGroup>
<Target Name="GenerateNuGetPackage" AfterTargets="CopyNativeBinary">
<Exec Command="dotnet pack --no-build --nologo" UseUtf8Encoding="Always" EchoOff="true" />
</Target>
</Project>

Binary file not shown.

View File

@@ -0,0 +1,166 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Yae.Utilities;
namespace Yae;
internal static unsafe class Application {
private static bool _initialized;
[UnmanagedCallersOnly(EntryPoint = "YaeMain")]
private static uint Awake(nint hModule) {
if (Interlocked.Exchange(ref _initialized, true)) {
return 1;
}
Native.RegisterUnhandledExceptionHandler();
Log.UseConsoleOutput();
Log.Trace("~");
Goshujin.Init();
Goshujin.LoadCmdTable();
Goshujin.LoadMethodTable();
Goshujin.ResumeMainThread();
//
Native.WaitMainWindow();
Log.ResetConsole();
//
RecordChecksum();
MinHook.Attach(GameMethod.DoCmd, &OnDoCmd, out _doCmd);
MinHook.Attach(GameMethod.ToUInt16, &OnToUInt16, out _toUInt16);
MinHook.Attach(GameMethod.UpdateNormalProp, &OnUpdateNormalProp, out _updateNormalProp);
MinHook.Attach(GameMethod.EventSystemUpdate, &OnEventSystemUpdate, out _eventSystemUpdate);
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "YaeWndHook")]
private static nint WndHook(int nCode, nint wParam, nint lParam) {
((delegate*unmanaged<nint, uint>) &Awake)(0);
return User32.CallNextHookEx(0, nCode, wParam, lParam);
}
#region RecvPacket
private static delegate*unmanaged<byte*, int, ushort> _toUInt16;
[UnmanagedCallersOnly]
private static ushort OnToUInt16(byte* val, int startIndex) {
var ret = _toUInt16(val, startIndex);
if (ret != 0xAB89 || *(ushort*) (val += 0x20) != 0x6745) {
return ret;
}
var cmdId = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 2));
if (cmdId == CmdId.PlayerStoreNotify) {
Goshujin.PushStoreData(GetData(val));
} else if (cmdId == CmdId.AchievementAllDataNotify) {
Goshujin.PushAchievementData(GetData(val));
}
return ret;
static Span<byte> GetData(byte* val) {
var headLen = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 4));
var dataLen = BinaryPrimitives.ReverseEndianness(*(uint*) (val + 6));
return new Span<byte>(val + 10 + headLen, (int) dataLen);
}
}
#endregion
#region Prop
/*
* 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,
*/
public static HashSet<int> RequiredPlayerProperties { get; } = [
10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058
];
private static delegate*unmanaged<nint, int, double, double, int, void> _updateNormalProp;
[UnmanagedCallersOnly]
private static void OnUpdateNormalProp(nint @this, int type, double value, double lastValue, int state) {
_updateNormalProp(@this, type, value, lastValue, state);
if (RequiredPlayerProperties.Remove(type)) {
Goshujin.PushPlayerProp(type, value);
}
}
#endregion
#region Checksum
[StructLayout(LayoutKind.Sequential)]
private struct RecordChecksumCmdData {
public int Type;
public void* Buffer;
public int Length;
}
private static readonly RecordChecksumCmdData[] RecordedChecksum = new RecordChecksumCmdData[3];
private static void RecordChecksum() {
for (var i = 0; i < 3; i++) {
var buffer = NativeMemory.AllocZeroed(256);
var data = new RecordChecksumCmdData {
Type = i,
Buffer = buffer,
Length = 256
};
_ = GameMethod.DoCmd(23, Unsafe.AsPointer(ref data), sizeof(RecordChecksumCmdData));
RecordedChecksum[i] = data;
//REPL//Log.Trace($"nType={i}, value={new string((sbyte*) buffer, 0, data.Length)}");
}
}
private static delegate*unmanaged<int, void*, int, int> _doCmd;
[UnmanagedCallersOnly]
public static int OnDoCmd(int cmdType, void* data, int size) {
var result = _doCmd(cmdType, data, size);
if (cmdType == 23) {
var cmdData = (RecordChecksumCmdData*) data;
if (cmdData->Type < 3) {
var recordedData = RecordedChecksum[cmdData->Type];
cmdData->Length = recordedData.Length;
Buffer.MemoryCopy(recordedData.Buffer, cmdData->Buffer, recordedData.Length, recordedData.Length);
//REPL//Log.Trace($"Override type {cmdData->Type} result");
}
}
return result;
}
#endregion
#region EnterGate
private static long _lastTryEnterTime;
private static delegate*unmanaged<nint, void> _eventSystemUpdate;
[UnmanagedCallersOnly]
public static void OnEventSystemUpdate(nint @this) {
_eventSystemUpdate(@this);
if (Environment.TickCount64 - _lastTryEnterTime > 200) {
var obj = GameMethod.FindGameObject(GameMethod.NewString("BtnStart"u8.AsPointer()));
if (obj != 0 && GameMethod.SimulatePointerClick(@this, obj)) {
MinHook.Detach((nint) GameMethod.EventSystemUpdate);
}
_lastTryEnterTime = Environment.TickCount64;
}
}
#endregion
}

View File

@@ -0,0 +1,109 @@
using System.IO.Pipes;
using Yae.Utilities;
namespace Yae;
internal static class CmdId {
public static uint AchievementAllDataNotify { get; set; }
public static uint PlayerStoreNotify { get; set; }
}
internal static unsafe class GameMethod {
public static delegate*unmanaged<int, void*, int, int> DoCmd { get; set; }
public static delegate*unmanaged<byte*, int, ushort> ToUInt16 { get; set; }
public static delegate*unmanaged<nint, int, double, double, int, void> UpdateNormalProp { get; set; }
public static delegate*unmanaged<nint, nint> NewString { get; set; }
public static delegate*unmanaged<nint, nint> FindGameObject { get; set; }
public static delegate*unmanaged<nint, void> EventSystemUpdate { get; set; }
public static delegate*unmanaged<nint, nint, bool> SimulatePointerClick { get; set; }
}
internal static class Goshujin {
private static NamedPipeClientStream _pipeStream = null!;
private static BinaryReader _pipeReader = null!;
private static BinaryWriter _pipeWriter = null!;
private static Lock _lock = null!;
public static void Init(string pipeName = "YaeAchievementPipe") {
_lock = new Lock();
_pipeStream = new NamedPipeClientStream(pipeName);
_pipeReader = new BinaryReader(_pipeStream);
_pipeWriter = new BinaryWriter(_pipeStream);
_pipeStream.Connect();
Log.Trace("Pipe server connected.");
}
public static void PushAchievementData(Span<byte> data) {
using (_lock.EnterScope()) {
_pipeWriter.Write((byte) 1);
_pipeWriter.Write(data.Length);
_pipeWriter.Write(data);
_achievementDataPushed = true;
ExitIfFinished();
}
}
public static void PushStoreData(Span<byte> data) {
using (_lock.EnterScope()) {
_pipeWriter.Write((byte) 2);
_pipeWriter.Write(data.Length);
_pipeWriter.Write(data);
_storeDataPushed = true;
ExitIfFinished();
}
}
public static void PushPlayerProp(int type, double value) {
using (_lock.EnterScope()) {
_pipeWriter.Write((byte) 3);
_pipeWriter.Write(type);
_pipeWriter.Write(value);
ExitIfFinished();
}
}
public static void LoadCmdTable() {
_pipeWriter.Write((byte) 0xFC);
CmdId.AchievementAllDataNotify = _pipeReader.ReadUInt32();
CmdId.PlayerStoreNotify = _pipeReader.ReadUInt32();
}
public static unsafe void LoadMethodTable() {
_pipeWriter.Write((byte) 0xFD);
GameMethod.DoCmd = (delegate*unmanaged<int, void*, int, int>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.ToUInt16 = (delegate*unmanaged<byte*, int, ushort>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.UpdateNormalProp = (delegate*unmanaged<nint, int, double, double, int, void>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.NewString = (delegate*unmanaged<nint, nint>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.FindGameObject = (delegate*unmanaged<nint, nint>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.EventSystemUpdate = (delegate*unmanaged<nint, void>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.SimulatePointerClick = (delegate*unmanaged<nint, nint, bool>) Native.RVAToVA(_pipeReader.ReadUInt32());
}
public static void ResumeMainThread() {
_pipeWriter.Write((byte) 0xFE);
}
private static bool _storeDataPushed;
private static bool _achievementDataPushed;
private static void ExitIfFinished() {
if (_storeDataPushed && _achievementDataPushed && Application.RequiredPlayerProperties.Count == 0) {
_pipeWriter.Write((byte) 0xFF);
_pipeReader.ReadBoolean();
Environment.Exit(0);
}
}
}

View File

@@ -0,0 +1,111 @@
using System.Runtime.CompilerServices;
// ReSharper disable MemberCanBePrivate.Global
namespace Yae.Utilities;
[Flags]
internal enum LogLevel : byte {
Trace = 0x00,
Debug = 0x01,
Info = 0x02,
Warn = 0x03,
Error = 0x04,
Fatal = 0x05,
Time = 0x06,
LevelMask = 0x0F,
FileOnly = 0x10,
}
internal static class Log {
#region ConsoleWriter
private static TextWriter? _consoleWriter;
[Conditional("EnableLogging")]
public static void UseConsoleOutput() {
InitializeConsole();
_consoleWriter = Console.Out;
}
[Conditional("EnableLogging")]
public static void ResetConsole() {
Kernel32.FreeConsole();
InitializeConsole();
var sw = new StreamWriter(Console.OpenStandardOutput(), _consoleWriter!.Encoding, 256, true) {
AutoFlush = true
};
_consoleWriter = TextWriter.Synchronized(sw);
Console.SetOut(_consoleWriter);
}
private static unsafe void InitializeConsole() {
Kernel32.AllocConsole();
uint mode;
var cHandle = Kernel32.GetStdHandle(Kernel32.STD_OUTPUT_HANDLE);
if (!Kernel32.GetConsoleMode(cHandle, &mode)) {
return;
}
Kernel32.SetConsoleMode(cHandle, mode | Kernel32.ENABLE_VIRTUAL_TERMINAL_PROCESSING);
Console.OutputEncoding = Console.InputEncoding = System.Text.Encoding.UTF8;
}
#endregion
[DoesNotReturn]
public static void ErrorAndExit(string value, [CallerMemberName] string callerName = "") {
WriteLog(value, callerName, LogLevel.Fatal);
Environment.Exit(0);
}
public static void Error(string value, [CallerMemberName] string callerName = "") {
WriteLog(value, callerName, LogLevel.Error);
}
public static void Warn(string value, [CallerMemberName] string callerName = "") {
WriteLog(value, callerName, LogLevel.Warn);
}
public static void Info(string value, [CallerMemberName] string callerName = "") {
WriteLog(value, callerName, LogLevel.Info);
}
public static void Debug(string value, [CallerMemberName] string callerName = "") {
WriteLog(value, callerName, LogLevel.Debug);
}
public static void Trace(string value, [CallerMemberName] string callerName = "") {
WriteLog(value, callerName, LogLevel.Trace);
}
public static void Time(string value, [CallerMemberName] string callerName = "") {
WriteLog(value, callerName, LogLevel.Time);
}
[Conditional("EnableLogging")]
public static void WriteLog(string message, string tag, LogLevel level) {
var time = DateTimeOffset.Now.ToString("HH:mm:ss.fff");
if (_consoleWriter != null) {
var color = level switch {
LogLevel.Error or LogLevel.Fatal => "244;67;54",
LogLevel.Warn => "255;235;59",
LogLevel.Info => "153;255;153",
LogLevel.Debug => "91;206;250",
LogLevel.Trace => "246;168;184",
LogLevel.Time => "19;161;14",
_ => throw new ArgumentException($"Invalid log level: {level}")
};
_consoleWriter.Write($"[{time}][\e[38;2;{color}m{level,5}\e[0m] {tag} : ");
_consoleWriter.WriteLine(message);
}
if (level == LogLevel.Fatal) {
if (_consoleWriter != null) {
WriteLog("Error occurred, press enter key to exit", tag, LogLevel.Error);
Console.ReadLine();
} else {
User32.MessageBoxW(0, "An critical error occurred.", "Error", 0x10);
}
}
}
}

View File

@@ -0,0 +1,181 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// ReSharper disable MemberCanBePrivate.Global
namespace Yae.Utilities;
internal static unsafe class Native {
#region WaitMainWindow
private static nint _hwnd;
private static readonly uint ProcessId = Kernel32.GetCurrentProcessId();
public static void WaitMainWindow() {
_hwnd = 0;
do {
Thread.Sleep(100);
_ = User32.EnumWindows(&EnumWindowsCallback, 0);
} while (_hwnd == 0);
return;
[UnmanagedCallersOnly(CallConvs = [ typeof(CallConvStdcall) ])]
static int EnumWindowsCallback(nint handle, nint extraParameter) {
uint wProcessId = 0; // Avoid uninitialized variable if the window got closed in the meantime
_ = User32.GetWindowThreadProcessId(handle, &wProcessId);
var cName = (char*) NativeMemory.Alloc(256);
if (User32.GetClassNameW(handle, cName, 256) != 0) {
if (wProcessId == ProcessId && User32.IsWindowVisible(handle) && new string(cName) == "UnityWndClass") {
_hwnd = handle;
}
}
NativeMemory.Free(cName);
return _hwnd == 0 ? 1 : 0;
}
}
#endregion
#region RestoreVirtualProtect
public static bool RestoreVirtualProtect() {
// NtProtectVirtualMemoryImpl
// _ = stackalloc byte[] { 0x4C, 0x8B, 0xD1, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x05, 0xC3 };
if (!NativeLibrary.TryLoad("ntdll.dll", out var hPtr)) {
return false;
}
if (!NativeLibrary.TryGetExport(hPtr, "NtProtectVirtualMemory", out var mPtr)) {
return false;
}
// 4C 8B D1 mov r10, rcx
// B8 mov eax, $imm32
if (*(uint*) (mPtr - 0x20) != 0xB8D18B4C) { // previous
return false;
}
var syscallNumber = (ulong) *(uint*) (mPtr - 0x1C) + 1;
var old = 0u;
if (!Kernel32.VirtualProtect(mPtr, 1, Kernel32.PAGE_EXECUTE_READWRITE, &old)) {
return false;
}
*(ulong*) mPtr = 0xB8D18B4C | syscallNumber << 32;
return Kernel32.VirtualProtect(mPtr, 1, old, &old);
}
#endregion
#region GetModuleHandle
public static string GetModulePath(nint hModule) {
var buffer = stackalloc char[256];
_ = Kernel32.GetModuleFileNameW(hModule, buffer, 256);
return new string(buffer);
}
public static nint GetModuleHandle(string? moduleName = null) {
fixed (char* pName = moduleName ?? Path.GetFileName(GetModulePath(0))) {
return Kernel32.GetModuleHandleW(pName);
}
}
#endregion
private static readonly nint ModuleBase = GetModuleHandle();
public static nint RVAToVA(uint addr) => ModuleBase + (nint) addr;
public static void RegisterUnhandledExceptionHandler() {
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
return;
static void OnUnhandledException(object? sender, UnhandledExceptionEventArgs e) {
var ex = e.ExceptionObject as Exception;
User32.MessageBoxW(0, ex?.ToString() ?? "null", "Unhandled Exception", 0x10);
Environment.Exit(-1);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint AsPointer(this ReadOnlySpan<byte> span) => *(nint*) Unsafe.AsPointer(ref span);
}
internal static partial class MinHook {
/// <summary>
/// Initialize the MinHook library. You must call this function EXACTLY ONCE at the beginning of your program.
/// </summary>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_Initialize")]
private static partial uint MinHookInitialize();
/// <summary>
/// Creates a hook for the specified target function, in disabled state.
/// </summary>
/// <param name="pTarget">A pointer to the target function, which will be overridden by the detour function.</param>
/// <param name="pDetour">A pointer to the detour function, which will override the target function.</param>
/// <param name="ppOriginal">
/// A pointer to the trampoline function, which will be used to call the original target function.
/// This parameter can be NULL.
/// </param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_CreateHook")]
private static partial uint MinHookCreate(nint pTarget, nint pDetour, out nint ppOriginal);
/// <summary>
/// Enables an already created hook.
/// </summary>
/// <param name="pTarget">
/// A pointer to the target function.
/// If this parameter is MH_ALL_HOOKS, all created hooks are enabled in one go.
/// </param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_EnableHook")]
private static partial uint MinHookEnable(nint pTarget);
/// <summary>
/// Disables an already created hook.
/// </summary>
/// <param name="pTarget">
/// A pointer to the target function.
/// If this parameter is MH_ALL_HOOKS, all created hooks are enabled in one go.
/// </param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_DisableHook")]
private static partial uint MinHookDisable(nint pTarget);
/// <summary>
/// Removes an already created hook.
/// </summary>
/// <param name="pTarget">A pointer to the target function.</param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_RemoveHook")]
private static partial uint MinHookRemove(nint pTarget);
/// <summary>
/// Uninitialize the MinHook library. You must call this function EXACTLY ONCE at the end of your program.
/// </summary>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_Uninitialize")]
// ReSharper disable once UnusedMember.Local
private static partial uint MinHookUninitialize();
static MinHook() {
var result = MinHookInitialize();
if (result != 0) {
throw new InvalidOperationException($"Failed to initialize MinHook: {result}");
}
}
public static void Attach(nint origin, nint handler, out nint trampoline) {
uint result;
if ((result = MinHookCreate(origin, handler, out trampoline)) != 0) {
throw new InvalidOperationException($"Failed to create hook: {result}");
}
if ((result = MinHookEnable(origin)) != 0) {
throw new InvalidOperationException($"Failed to enable hook: {result}");
}
}
public static void Detach(nint origin) {
uint result;
if ((result = MinHookDisable(origin)) != 0) {
throw new InvalidOperationException($"Failed to create hook: {result}");
}
if ((result = MinHookRemove(origin)) != 0) {
throw new InvalidOperationException($"Failed to enable hook: {result}");
}
}
}

View File

@@ -0,0 +1,74 @@
using System.Runtime.InteropServices;
namespace Yae.Utilities;
#pragma warning disable CS0649, CA1069 // ReSharper disable IdentifierTypo, InconsistentNaming, UnassignedField.Global
internal static unsafe partial class Kernel32 {
[LibraryImport("KERNEL32.dll")]
internal static partial uint GetCurrentProcessId();
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial nint GetModuleHandleW(char* lpModuleName);
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial uint GetModuleFileNameW(nint hModule, char* lpFilename, uint nSize);
internal const uint PAGE_EXECUTE_READWRITE = 0x00000040;
[return:MarshalAs(UnmanagedType.I4)]
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, uint* lpflOldProtect);
internal const uint STD_OUTPUT_HANDLE = 0xFFFFFFF5;
internal const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x00000004;
[return:MarshalAs(UnmanagedType.I4)]
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial bool AllocConsole();
[return:MarshalAs(UnmanagedType.I4)]
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial bool FreeConsole();
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial nint GetStdHandle(uint nStdHandle);
[return:MarshalAs(UnmanagedType.I4)]
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial bool GetConsoleMode(nint hConsoleHandle, uint* lpMode);
[return:MarshalAs(UnmanagedType.I4)]
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial nint CreateThread(nint lpThreadAttributes, nint dwStackSize, delegate*unmanaged<nint, uint> lpStartAddress, nint lpParameter, uint dwCreationFlags, uint* lpThreadId);
}
internal static unsafe partial class User32 {
[LibraryImport("USER32.dll", SetLastError = true)]
internal static partial uint GetWindowThreadProcessId(nint hWnd, uint* lpdwProcessId);
[LibraryImport("USER32.dll", SetLastError = true)]
internal static partial int GetClassNameW(nint hWnd, char* lpClassName, int nMaxCount);
[return: MarshalAs(UnmanagedType.I4)]
[LibraryImport("USER32.dll")]
internal static partial bool IsWindowVisible(nint hWnd);
[return: MarshalAs(UnmanagedType.I4)]
[LibraryImport("USER32.dll", SetLastError = true)]
internal static partial bool EnumWindows(delegate *unmanaged[Stdcall]<nint, nint, int> lpEnumFunc, nint lParam);
[LibraryImport("USER32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial int MessageBoxW(nint hWnd, string text, string caption, uint uType);
[LibraryImport("USER32.dll")]
internal static partial nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
}

4
lib/.gitignore vendored
View File

@@ -1,4 +0,0 @@
.vs
build
YaeAchievementLib.vcxproj.user
YaeAchievementLib.vcxproj.filters

View File

@@ -1,29 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32407.343
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "YaeAchievementLib", "YaeAchievementLib.vcxproj", "{83C3DF1A-6219-408E-98A3-C7040CCC96FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x64.ActiveCfg = Debug|x64
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x64.Build.0 = Debug|x64
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x86.ActiveCfg = Debug|x64
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x64.ActiveCfg = Release|x64
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x64.Build.0 = Release|x64
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x86.ActiveCfg = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {470905A4-E6C4-4363-B44D-BAE9A50755A3}
EndGlobalSection
EndGlobal

View File

@@ -1,132 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{83c3df1a-6219-408e-98a3-c7040ccc96fd}</ProjectGuid>
<RootNamespace>YaeAchievementLib</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<EnableASAN>false</EnableASAN>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
<TargetName>YaeLib</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
<AdditionalIncludeDirectories>$(ProjectDir)lib\detours\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>$(ProjectDir)lib\detours\;$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>detours-x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net6.0</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_AMD64_;NDEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
<DebugInformationFormat>None</DebugInformationFormat>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<AdditionalIncludeDirectories>$(ProjectDir)lib\detours\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>$(ProjectDir)lib\detours\;$(OutDir)</AdditionalLibraryDirectories>
<AdditionalDependencies>detours-x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net8.0-windows\win-x64\YaeAchievementLib.dll /y</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="src\HookManager.h" />
<ClInclude Include="src\il2cpp-appdata.h" />
<ClInclude Include="src\il2cpp-functions.h" />
<ClInclude Include="src\il2cpp-types.h" />
<ClInclude Include="src\il2cpp-init.h" />
<ClInclude Include="src\pch.h" />
<ClInclude Include="src\util.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\dllmain.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" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

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

View File

@@ -1,82 +0,0 @@
// ReSharper disable CppCStyleCast
// ReSharper disable CppInconsistentNaming
// ReSharper disable CppClangTidyModernizeUseStdPrint
// ReSharper disable CppClangTidyClangDiagnosticCastAlign
// ReSharper disable CppClangTidyHicppMultiwayPathsCovered
// ReSharper disable CppDefaultCaseNotHandledInSwitchStatement
// ReSharper disable CppClangTidyClangDiagnosticCastFunctionTypeStrict
#include "pch.h"
#include "util.h"
#include "il2cpp-init.h"
using Genshin::ByteArray;
HWND unityWnd = nullptr;
HANDLE hPipe = nullptr;
void* baClass;
std::string checksum;
namespace Hook {
ByteArray* UnityEngine_RecordUserData(const INT type) {
if (type == 0) {
const auto len = checksum.length();
const auto arr = Genshin::il2cpp_array_new_specific(baClass, len);
memcpy(&arr->vector[0], checksum.data(), len);
return arr;
}
return Genshin::il2cpp_array_new_specific(baClass, 0);
}
uint16_t BitConverter_ToUInt16(ByteArray* val, const int startIndex) {
const auto ret = CALL_ORIGIN(BitConverter_ToUInt16, val, startIndex);
if (ret == 0xAB89 && ReadMapped<UINT16>(val->vector, 2) == 24082) {
const auto headLength = ReadMapped<UINT16>(val->vector, 4);
const auto dataLength = ReadMapped<UINT32>(val->vector, 6);
const auto cStr = base64_encode(val->vector + 10 + headLength, dataLength) + "\n";
WriteFile(hPipe, cStr.c_str(), (DWORD) cStr.length(), nullptr, nullptr);
CloseHandle(hPipe);
ExitProcess(0);
}
return ret;
}
}
void Run(HMODULE* phModule) {
//AllocConsole();
//freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
while ((unityWnd = FindMainWindowByPID(GetCurrentProcessId())) == nullptr) {
Sleep(1000);
}
Sleep(5000);
DisableVMProtect();
InitIL2CPP();
for (int i = 0; i < 3; i++) {
const auto result = Genshin::RecordUserData(i);
checksum += string(reinterpret_cast<char*>(&result->vector[0]), result->max_length);
baClass = result->klass;
}
HookManager::install(Genshin::RecordUserData, Hook::UnityEngine_RecordUserData);
HookManager::install(Genshin::BitConverter_ToUInt16, Hook::BitConverter_ToUInt16);
hPipe = CreateFile(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (hPipe == INVALID_HANDLE_VALUE) {
Win32ErrorDialog(1001);
ExitProcess(0);
}
}
// DLL entry point
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReasonForCall, LPVOID lpReserved) {
switch (ulReasonForCall) {
case DLL_PROCESS_ATTACH:
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Run, new HMODULE(hModule), 0, NULL);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

View File

@@ -1,11 +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
namespace Genshin {
#include "il2cpp-functions.h"
}
#undef DO_APP_FUNC

View File

@@ -1,9 +0,0 @@
using namespace Genshin;
// DO_APP_FUNC(CN_OFFSET, OS_OFFSET, RETURN, FUNC_NAME, (ARGS...));
DO_APP_FUNC(0x002ED400, 0x002ED390, ByteArray*, il2cpp_array_new_specific, (void* arrayTypeInfo, uint64_t length));
DO_APP_FUNC(0x0113ADC0, 0x0113AFC0, ByteArray*, RecordUserData, (int32_t nType));
DO_APP_FUNC(0x0F826CF0, 0x0F825F10, uint16_t, BitConverter_ToUInt16, (ByteArray* val, int startIndex));

View File

@@ -1,27 +0,0 @@
// ReSharper disable CppCStyleCast
// ReSharper disable CppInconsistentNaming
// ReSharper disable CppClangTidyBugproneMacroParentheses
// ReSharper disable CppClangTidyClangDiagnosticCastAlign
#include "pch.h"
#include "il2cpp-init.h"
#define DO_APP_FUNC(ca, oa, r, n, p) r (*n) p
namespace Genshin {
#include "il2cpp-functions.h"
}
#undef DO_APP_FUNC
using std::string;
void InitIL2CPP() {
TCHAR szFileName[MAX_PATH];
GetModuleFileName(nullptr, szFileName, MAX_PATH);
_strupr_s(szFileName);
const auto isCN = strstr(szFileName, "YUANSHEN.EXE");
const auto uBase = reinterpret_cast<uint64_t>(GetModuleHandle(isCN ? "YuanShen.exe" : "GenshinImpact.exe"));
#define DO_APP_FUNC(ca, oa, r, n, p) n = (r (*) p)(uBase + (isCN ? ca : oa))
#include "il2cpp-functions.h"
#undef DO_APP_FUNC
}

View File

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

View File

@@ -1,15 +0,0 @@
// ReSharper disable CppClangTidyClangDiagnosticReservedIdentifier
// ReSharper disable CppClangTidyBugproneReservedIdentifier
#pragma once
namespace Genshin {
struct ByteArray {
void* klass;
void* monitor;
void* bounds;
uint64_t max_length;
uint8_t vector[32];
};
}

View File

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

View File

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

View File

@@ -1,93 +0,0 @@
#include "pch.h"
#include "util.h"
VOID DisableVMProtect() {
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 ByteUtils
bool IsLittleEndian() {
UINT i = 1;
char* c = (char*)&i;
return *c;
}
#pragma endregion
#pragma region FindMainWindowByPID
struct HandleData {
DWORD pid;
HWND hwnd;
};
BOOL IsMainWindow(HWND handle) {
return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle) == TRUE;
}
BOOL IsUnityWindow(HWND handle) {
TCHAR name[256];
GetClassName(handle, name, 256);
return _strcmpi(name, "UnityWndClass") == 0;
}
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
HandleData& data = *(HandleData*)lParam;
DWORD pid = 0;
GetWindowThreadProcessId(handle, &pid);
if (data.pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle))
return TRUE;
data.hwnd = handle;
return FALSE;
}
HWND FindMainWindowByPID(DWORD pid) {
HandleData data = { pid, 0 };
EnumWindows(EnumWindowsCallback, (LPARAM)&data);
return data.hwnd;
}
#pragma endregion
static const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string base64_encode(BYTE const* buf, unsigned int bufLen) {
std::string ret;
int i = 0;
BYTE char_array_3[3];
BYTE char_array_4[4];
while (bufLen--) {
char_array_3[i++] = *buf++;
if (i == 3) {
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 (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;
}

View File

@@ -1,25 +0,0 @@
#pragma once
using std::string;
VOID DisableVMProtect();
bool IsLittleEndian();
HWND FindMainWindowByPID(DWORD pid);
std::string base64_encode(BYTE const* buf, unsigned int bufLen);
#define ErrorDialogT(title, msg) MessageBox(unityWnd, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL)
#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 = 11;
uint32 current = 5;
uint32 total = 15;
uint32 id = 2;
Status status = 7;
}
message AchievementAllDataNotify {
repeated Achievement list = 4;
}

View File

@@ -1,16 +0,0 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message AchievementItem {
uint32 pre = 1;
uint32 group = 2;
string name = 3;
string description = 4;
}
message AchievementInfo {
string version = 1;
map<uint32, string> group = 2;
map<uint32, AchievementItem> items = 3;
}

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,258 +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().Items.ToDictionary(pair => pair.Key, pair => pair.Value.Group);
var final = new Dictionary<string, Dictionary<uint, Dictionary<uint, bool>>> {
["achievement"] = data.List
.Where(achievement => achievement.Status is Status.Finished or Status.RewardTaken)
.Where(achievement => info.ContainsKey(achievement.Id))
.GroupBy(achievement => info[achievement.Id], achievement => achievement.Id)
.OrderBy(group => group.Key)
.ToDictionary(group => group.Key, group => group.ToDictionary(id => id, _ => true))
};
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] = 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,50 +0,0 @@
using System.Text;
using Proto;
using YaeAchievement;
using YaeAchievement.res;
using static YaeAchievement.Utils;
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
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 ") || name.Contains("Microsoft Visual C++ 2015-2022 Redistributable (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);
}
}
}