mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-08 23:48:12 +08:00
Compare commits
338 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19f84bdb86 | ||
|
|
9d22fdf6f9 | ||
|
|
27fa0d9c84 | ||
|
|
e9baf8f211 | ||
|
|
b960165b7e | ||
|
|
a4c2027ada | ||
|
|
f49477c49a | ||
|
|
67c2fb3bda | ||
|
|
be3440695d | ||
|
|
f8b8a5a9e1 | ||
|
|
49f8679996 | ||
|
|
222a26233e | ||
|
|
1130f442fc | ||
|
|
b80987f574 | ||
|
|
c96395e1a2 | ||
|
|
3f42156b20 | ||
|
|
45638b7327 | ||
|
|
d514f3b5e7 | ||
|
|
3fe54d908e | ||
|
|
48043a9deb | ||
|
|
c09e008d38 | ||
|
|
ac818735c7 | ||
|
|
c4fb5a0366 | ||
|
|
0522078582 | ||
|
|
97d063ec76 | ||
|
|
a6caa07599 | ||
|
|
7fc296f6e9 | ||
|
|
1216ffb872 | ||
|
|
f8341c3044 | ||
|
|
40269410c5 | ||
|
|
a32e27323c | ||
|
|
4a1da61904 | ||
|
|
9eb8955fda | ||
|
|
62c08f54ab | ||
|
|
645fe38c65 | ||
|
|
8f9a26a237 | ||
|
|
8648b3a308 | ||
|
|
829553b3a6 | ||
|
|
87898eedfa | ||
|
|
a10b491886 | ||
|
|
e3e7107b14 | ||
|
|
3231746aa5 | ||
|
|
5c9cdd46d2 | ||
|
|
881a4bc725 | ||
|
|
d08ac17d10 | ||
|
|
980a47bf43 | ||
|
|
0e7be25b23 | ||
|
|
4b052cf6c7 | ||
|
|
4ff2b454f3 | ||
|
|
f718687b3f | ||
|
|
9915b9246e | ||
|
|
e25bc9aeba | ||
|
|
08dd6eca76 | ||
|
|
acd2ccd803 | ||
|
|
13fda3ba12 | ||
|
|
1c821620bf | ||
|
|
8a39ad0a77 | ||
|
|
9d42141258 | ||
|
|
2a91656b2e | ||
|
|
602cf06a8b | ||
|
|
c87b8c976d | ||
|
|
099c22e9e7 | ||
|
|
f1fe6e1f9e | ||
|
|
fb0a46480f | ||
|
|
7b413384c3 | ||
|
|
e251497edc | ||
|
|
b1f5d9a2b2 | ||
|
|
36a5b9a0e7 | ||
|
|
b0b70d585a | ||
|
|
b9ab326d72 | ||
|
|
5f210d4236 | ||
|
|
826063da60 | ||
|
|
08697941d3 | ||
|
|
e7d21865c7 | ||
|
|
35773f49f4 | ||
|
|
c4856c821f | ||
|
|
6f1168e61e | ||
|
|
d1bd0c7d7b | ||
|
|
95ad187015 | ||
|
|
6c0264ce5a | ||
|
|
957c8b98e4 | ||
|
|
01a3f41323 | ||
|
|
32ceae074e | ||
|
|
2e2be07161 | ||
|
|
832c82f44e | ||
|
|
43b38df986 | ||
|
|
6e1c8f275f | ||
|
|
e65f046520 | ||
|
|
4025729677 | ||
|
|
07050c1c3d | ||
|
|
e56a6228aa | ||
|
|
66b29b1374 | ||
|
|
247c401a5b | ||
|
|
cf9d601b27 | ||
|
|
5abb9e2934 | ||
|
|
1b861712eb | ||
|
|
d62377ad96 | ||
|
|
fb4e3f8d00 | ||
|
|
b1135542c1 | ||
|
|
7d3d0f5e14 | ||
|
|
4268b04f3c | ||
|
|
9a9d1310a1 | ||
|
|
618a9189ad | ||
|
|
f73dbdc4fe | ||
|
|
298134c063 | ||
|
|
e9ace26d69 | ||
|
|
2c15353f86 | ||
|
|
99fec63867 | ||
|
|
bf5525d2ea | ||
|
|
cf3749f887 | ||
|
|
21af4de1a6 | ||
|
|
8e0fd2d27c | ||
|
|
0348cfa365 | ||
|
|
494eda32c2 | ||
|
|
975638c1ee | ||
|
|
793ad075fe | ||
|
|
c82a10353f | ||
|
|
f737122247 | ||
|
|
520167ef85 | ||
|
|
faee6f6121 | ||
|
|
06c5468118 | ||
|
|
b7c2204f68 | ||
|
|
5dc5e646d6 | ||
|
|
9cab7e8702 | ||
|
|
1f080fe084 | ||
|
|
c8497243c0 | ||
|
|
9abdd123ee | ||
|
|
e1429289ad | ||
|
|
1f311ed987 | ||
|
|
cc346915e3 | ||
|
|
cd0f49d83d | ||
|
|
d0b7d15894 | ||
|
|
504c8a2a9a | ||
|
|
b3162052da | ||
|
|
034d999d25 | ||
|
|
45d5620e83 | ||
|
|
fa13f9c8e5 | ||
|
|
2210a97d61 | ||
|
|
feb7ac44da | ||
|
|
3924129560 | ||
|
|
4f7f0cdfd2 | ||
|
|
cf0753c676 | ||
|
|
0b895d47ca | ||
|
|
78d2722e20 | ||
|
|
385c673323 | ||
|
|
50beb2cce7 | ||
|
|
324a4153e0 | ||
|
|
3de459aceb | ||
|
|
295bb89177 | ||
|
|
baaf4e8227 | ||
|
|
f41fe6fb12 | ||
|
|
78bda3f49c | ||
|
|
a10dc22461 | ||
|
|
74dda750ef | ||
|
|
099270ad29 | ||
|
|
fe5b2c0c12 | ||
|
|
ed5d99745c | ||
|
|
7175cd7427 | ||
|
|
73747bcce5 | ||
|
|
b12c3209d7 | ||
|
|
5805070627 | ||
|
|
811daba164 | ||
|
|
849f379ca5 | ||
|
|
29c57cbc1e | ||
|
|
8fdb10898a | ||
|
|
f4f1f14651 | ||
|
|
390af6cd85 | ||
|
|
a78f36dfa8 | ||
|
|
e3d7152da2 | ||
|
|
60a4dd04c4 | ||
|
|
69c402417b | ||
|
|
d9034ec635 | ||
|
|
f742cf37d5 | ||
|
|
4136a1b0c4 | ||
|
|
e039d2f944 | ||
|
|
0e23a05a78 | ||
|
|
5f5a0614a6 | ||
|
|
c60d3a3b82 | ||
|
|
58dcd5b228 | ||
|
|
a72007ffa6 | ||
|
|
d3b9d10d01 | ||
|
|
69184fa59d | ||
|
|
8473336b37 | ||
|
|
ffc0854291 | ||
|
|
3b2b1fba49 | ||
|
|
c325b5f754 | ||
|
|
8e2e438c96 | ||
|
|
9bc2cdb443 | ||
|
|
826ed661cd | ||
|
|
5f8ff734f2 | ||
|
|
50007c2c53 | ||
|
|
7512c6fca2 | ||
|
|
2feae6ddb9 | ||
|
|
52ae44f467 | ||
|
|
d93f6f92c0 | ||
|
|
20b59eab7e | ||
|
|
0094b9b959 | ||
|
|
24b68fbed1 | ||
|
|
397923d4ad | ||
|
|
0a3482e7b2 | ||
|
|
7ae18cfbf0 | ||
|
|
24cd49fa03 | ||
|
|
f0dbb9162b | ||
|
|
68ff9c5a25 | ||
|
|
31b77a9fb3 | ||
|
|
d2d5bafcd6 | ||
|
|
e2f1f1e343 | ||
|
|
349e15fe25 | ||
|
|
9e0d18910b | ||
|
|
afee99fd3f | ||
|
|
4dae8c52b6 | ||
|
|
3294ac4b89 | ||
|
|
ccb19c832c | ||
|
|
1207dd70b3 | ||
|
|
204f211249 | ||
|
|
043a861030 | ||
|
|
09a9d4c22b | ||
|
|
9094b9c718 | ||
|
|
d7ac18587a | ||
|
|
82c5054002 | ||
|
|
553d67bff7 | ||
|
|
07a08f56d4 | ||
|
|
656589bc80 | ||
|
|
9aaefe4cd7 | ||
|
|
d84571ba1c | ||
|
|
9bc8d2473a | ||
|
|
7b3e22c84f | ||
|
|
faa583587a | ||
|
|
37bfb93fa9 | ||
|
|
b88858d2dc | ||
|
|
c2e3a3b13d | ||
|
|
78a29e9390 | ||
|
|
a76f03b035 | ||
|
|
d814ece2de | ||
|
|
9414ffbe12 | ||
|
|
251246fd74 | ||
|
|
76785c5179 | ||
|
|
e086e14e8d | ||
|
|
29aa4fea2d | ||
|
|
ffd75da2bf | ||
|
|
92e8cd8997 | ||
|
|
4b62901dbe | ||
|
|
2216610413 | ||
|
|
99b8db2a69 | ||
|
|
5b6e86459d | ||
|
|
bf46494b6b | ||
|
|
458a56a855 | ||
|
|
ef21274cd2 | ||
|
|
7528f4247b | ||
|
|
652b5afa80 | ||
|
|
59a042019a | ||
|
|
4e94d67d0b | ||
|
|
7dafd95099 | ||
|
|
76c20407ea | ||
|
|
b79b82ec10 | ||
|
|
07fe60a092 | ||
|
|
7fa2fbac25 | ||
|
|
28ffa6fb1a | ||
|
|
4c2cb28313 | ||
|
|
2f1a5ad99e | ||
|
|
9b0c384d4b | ||
|
|
b2111db4eb | ||
|
|
2442264224 | ||
|
|
b596cad02e | ||
|
|
30a0189f5e | ||
|
|
f47fd234b4 | ||
|
|
7d306a60c9 | ||
|
|
10dd03335f | ||
|
|
41863c32f7 | ||
|
|
4c3e9d8e50 | ||
|
|
9d60cda4c7 | ||
|
|
e0c836e55d | ||
|
|
02b4d9df0b | ||
|
|
d5a20b44d5 | ||
|
|
31e23de4d6 | ||
|
|
fb8c941b57 | ||
|
|
468b4b91ea | ||
|
|
0ace6b951f | ||
|
|
74473a3811 | ||
|
|
34afe3b7a4 | ||
|
|
f1e8c09262 | ||
|
|
6a1fbe8fff | ||
|
|
e33ee57afc | ||
|
|
54763cbc80 | ||
|
|
1dcaf7ed8f | ||
|
|
bbe7c2cd03 | ||
|
|
e847d4c80e | ||
|
|
f753acfc78 | ||
|
|
08ccdb203e | ||
|
|
180ab8bab7 | ||
|
|
1adc6c4c0f | ||
|
|
82ab5a316c | ||
|
|
75c2f57cfa | ||
|
|
ef6a72312c | ||
|
|
3917f3e6c7 | ||
|
|
12348a3941 | ||
|
|
8aed3fd095 | ||
|
|
4110fc2d6d | ||
|
|
0de747e344 | ||
|
|
b76e8e3cd2 | ||
|
|
c12d376e21 | ||
|
|
d285b1c999 | ||
|
|
89ab4408d6 | ||
|
|
bf01214971 | ||
|
|
1900937a50 | ||
|
|
98a03a0910 | ||
|
|
82ba6120e4 | ||
|
|
6cb07d274a | ||
|
|
4acaa516bb | ||
|
|
b5caba2f7a | ||
|
|
ebb11bde9c | ||
|
|
4417feab53 | ||
|
|
28080dbafd | ||
|
|
a8b6419157 | ||
|
|
7f8296c3dc | ||
|
|
37382b28e0 | ||
|
|
a46e49722f | ||
|
|
700cbbb86d | ||
|
|
6a23153f70 | ||
|
|
75e3cd848f | ||
|
|
96912d3da7 | ||
|
|
02b034cc48 | ||
|
|
00898d11cb | ||
|
|
7cf03ad905 | ||
|
|
7d9e5bc218 | ||
|
|
60f0d8d23b | ||
|
|
1e15a49667 | ||
|
|
4528af7235 | ||
|
|
9850d1dbe4 | ||
|
|
b1f307de83 | ||
|
|
bea596b906 | ||
|
|
dd582437bc | ||
|
|
ea168ce96b | ||
|
|
90ab4dafe9 | ||
|
|
55f1ce3d55 | ||
|
|
0e51e080d4 | ||
|
|
01ab053d7d | ||
|
|
41af9c7cdb |
21
.gitattributes
vendored
Normal file
21
.gitattributes
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
lib/src/Zydis.* linguist-generated=true
|
||||
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
*.cs text diff=csharp
|
||||
*.cshtml text diff=html
|
||||
*.csx text diff=csharp
|
||||
*.sln text eol=crlf merge=union
|
||||
*.csproj text merge=union
|
||||
|
||||
# Sources
|
||||
*.c text diff=cpp
|
||||
*.cc text diff=cpp
|
||||
*.cxx text diff=cpp
|
||||
*.cpp text diff=cpp
|
||||
*.c++ text diff=cpp
|
||||
*.hpp text diff=cpp
|
||||
*.h text diff=cpp
|
||||
*.h++ text diff=cpp
|
||||
*.hh text diff=cpp
|
||||
40
.github/workflows/dotnet.yml
vendored
Normal file
40
.github/workflows/dotnet.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: .NET Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./YaeAchievement
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build -c Release --no-restore
|
||||
- name: Publish-AOT
|
||||
run: dotnet publish --property:OutputPath=.\publish\
|
||||
- name: Upload-AOT
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aot
|
||||
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
36
.github/workflows/lib-nuget.yml
vendored
Normal 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
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,11 +1,6 @@
|
||||
cache
|
||||
cert
|
||||
config.json
|
||||
out.*
|
||||
node_modules
|
||||
bin
|
||||
obj
|
||||
.idea
|
||||
app*.exe
|
||||
export*.json
|
||||
secret.js
|
||||
package-lock.json
|
||||
native.node
|
||||
.vs
|
||||
publish
|
||||
sync
|
||||
|
||||
64
.gitlab-ci.yml
Normal file
64
.gitlab-ci.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- release
|
||||
|
||||
Test:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/windows/server
|
||||
tags:
|
||||
- windows
|
||||
script:
|
||||
- dotnet restore
|
||||
- dotnet build -c Release --no-restore
|
||||
- dotnet publish --property:OutputPath=.\publish\
|
||||
- Move-Item -Path .\publish\publish\*.exe -Destination ..\ -Force
|
||||
|
||||
Build:
|
||||
stage: build
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- windows
|
||||
needs:
|
||||
- job: Test
|
||||
script:
|
||||
- echo "This is build stage."
|
||||
- Move-Item -Path ..\YaeAchievement.exe .\ -Force
|
||||
after_script:
|
||||
- echo "Current Job ID is $CI_JOB_ID"
|
||||
- echo "THIS_JOB_ID=$CI_JOB_ID" >> build.env
|
||||
artifacts:
|
||||
paths:
|
||||
- .\*.exe
|
||||
expire_in: 90 days
|
||||
reports:
|
||||
dotenv: build.env
|
||||
|
||||
|
||||
release:
|
||||
stage: release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
only:
|
||||
- tags
|
||||
needs:
|
||||
- job: Build
|
||||
artifacts: true
|
||||
variables:
|
||||
TAG: '$CI_COMMIT_TAG'
|
||||
script:
|
||||
- echo "Create Release $TAG"
|
||||
- echo "$THIS_JOB_ID"
|
||||
release:
|
||||
name: '$TAG'
|
||||
tag_name: '$TAG'
|
||||
ref: '$TAG'
|
||||
description: 'Release $TAG by CI'
|
||||
assets:
|
||||
links:
|
||||
- name: "YaeAchievement.exe"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/YaeAchievement.exe?inline=false"
|
||||
link_type: package
|
||||
- name: ".NET 7.0 Desktop Runtime"
|
||||
url: "https://dotnet.microsoft.com/zh-cn/download/dotnet/thank-you/runtime-desktop-7.0.11-windows-x64-installer"
|
||||
link_type: other
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "native/src/wmi"]
|
||||
path = native/src/wmi
|
||||
url = https://github.com/HolographicHat/wmi
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
|
||||
88
README.md
88
README.md
@@ -1,51 +1,37 @@
|
||||
# 原神成就导出工具
|
||||
|
||||
   
|
||||
|
||||
- 支持导出所有成就
|
||||
- 支持官服,B服与国际服
|
||||
- 没有窗口大小游戏或语言等要求
|
||||
- 更快、更准
|
||||
|
||||
## 使用说明
|
||||
**打开程序前需要关闭正在运行的原神主程序**
|
||||
第一次打开需要先设置原神主程序(YuanShen.exe/GenshinImpact.exe)所在路径
|
||||

|
||||
### Windows7
|
||||
系统变量添加名为```NODE_SKIP_PLATFORM_CHECK```的变量并将值设为```1```
|
||||
### 自定义代理(可选)
|
||||
配置文件内添加proxy字段,详细请参看[Axios-请求配置](https://axios-http.com/zh/docs/req_config)
|
||||
```json
|
||||
{
|
||||
"path": [],
|
||||
"offlineResource": false,
|
||||
"customCDN": "",
|
||||
"proxy": {
|
||||
"protocol": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": 7890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 下载地址
|
||||
[releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest)
|
||||
|
||||
## 问题反馈
|
||||
[issues](https://github.com/HolographicHat/genshin-achievement-export/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
|
||||
## 常见问题
|
||||
0. Q: 程序异常退出或被强行终止后,原神启动时报错: 无法连接网络(4201)
|
||||
A: 用文本编辑器打开```C:\Windows\System32\drivers\etc\hosts```,删除```127.0.0.1 dispatch**global.yuanshen.com```后保存,重启原神
|
||||
|
||||
1. Q: 原神启动时报错: 数据异常(31-4302)
|
||||
A: 不要把软件和原神主程序放一起
|
||||
|
||||
2. Q: 为什么需要管理员权限
|
||||
A: 临时修改Hosts和启动原神
|
||||
|
||||
3. Q: 报毒?
|
||||
A: 执行命令或修改hosts引发,相关代码可在./utils.js,./appcenter.js下找到
|
||||
|
||||
4. Q: 原神进门后没有自动退出,程序输出停留在“加载完成”
|
||||
A: 关闭Shadowsocks全局代理
|
||||
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
|
||||
|
||||
# YaeAchievement
|
||||
|
||||
    
|
||||
|
||||
简体中文 | [English](README_EN.md) | [日本語](README_JP.md)
|
||||
</div>
|
||||
|
||||
- 支持导出所有类别的成就
|
||||
- 支持官服,渠道服与国际服
|
||||
- 没有窗口大小、游戏语言等要求
|
||||
|
||||
## 导出支持
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. 表格文件 `.csv`
|
||||
5. [寻空](https://github.com/xunkong/xunkong)
|
||||
6. [原魔工具箱](https://apps.apple.com/app/id1663989619)
|
||||
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON 文件
|
||||
|
||||
## 使用说明
|
||||
→ [Tutorial.md](Tutorial.md)
|
||||
|
||||
## 下载地址
|
||||
[releases/latest](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## 问题反馈
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
|
||||
|
||||
## 常见问题
|
||||
1. Q: 原神启动时报错: 数据异常(31-4302)
|
||||
A: 不要把软件和原神主程序放一起
|
||||
|
||||
38
README_EN.md
Normal file
38
README_EN.md
Normal file
@@ -0,0 +1,38 @@
|
||||
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
|
||||
|
||||
# YaeAchievement
|
||||
|
||||
    
|
||||
|
||||
[简体中文](README.md) | English | [日本語](README_JP.md)
|
||||
|
||||
</div>
|
||||
|
||||
- Support for exporting all categories of achievements
|
||||
- Supports all versions of Genshin Impact
|
||||
- There are no requirements for window size, game language, etc.
|
||||
|
||||
## Export support
|
||||
|
||||
0. [Cocogoat](https://cocogoat.work/achievement)
|
||||
1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. Form File `.csv`
|
||||
5. [XunKong](https://github.com/xunkong/xunkong)
|
||||
6. [YuanmoTools](https://apps.apple.com/app/id1663989619)
|
||||
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON file
|
||||
|
||||
## Instructions for Use:
|
||||
→ [Tutorial_EN.md](Tutorial_EN.md)
|
||||
|
||||
## Download: [Here](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## Feedback or Problem?
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues) or [QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
|
||||
|
||||
## Frequently asked questions
|
||||
1. Q: Error while Genshin started: Data Exception (31-4302)
|
||||
A: Don't place software in the directory containing Genshin Impact.
|
||||
|
||||
38
README_JP.md
Normal file
38
README_JP.md
Normal file
@@ -0,0 +1,38 @@
|
||||
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
|
||||
|
||||
# YaeAchievement
|
||||
|
||||
    
|
||||
|
||||
[简体中文](README.md) | [English](README_EN.md) | 日本語
|
||||
|
||||
</div>
|
||||
|
||||
- すべてのカテゴリの実績のエクスポートをサポート
|
||||
- すべてのバージョンの原神をサポート
|
||||
- ウィンドウサイズ、ゲーム言語などの要件はありません
|
||||
|
||||
## エクスポートサポート
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. フォームファイル `.csv`
|
||||
5. [尋空](https://github.com/xunkong/xunkong)
|
||||
6. [原魔ツールボックス](https://apps.apple.com/app/id1663989619)
|
||||
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSONファイル
|
||||
|
||||
## 使用説明書:
|
||||
→ [Tutorial_JP.md](Tutorial_JP.md)
|
||||
|
||||
## ダウンロード: [こちら](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## フィードバックや問題?
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues) または [QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
|
||||
|
||||
## よくある質問
|
||||
1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302)
|
||||
A: ソフトウェアを原神のディレクトリに配置しないでください。
|
||||
|
||||
69
Tutorial.md
Normal file
69
Tutorial.md
Normal file
@@ -0,0 +1,69 @@
|
||||
## 使用说明
|
||||
|
||||
1.选择正确的下载文件(以2.4.1版本为例):
|
||||
|
||||
点击该网址:https://github.com/HolographicHat/YaeAchievement/releases
|
||||
|
||||
点击图中红框圈中的名称为“YaeAchievement.exe”的文件,浏览器会自动弹出下载。建议将该文件保存在桌面或者其它易于寻找的文件夹内。
|
||||
|
||||

|
||||
|
||||
2.打开主程序所需的操作以及成就导出的选择
|
||||
|
||||
双击在第一步下载的名称为“YaeAchievement.exe”的文件,成功打开后会提示原神正在启动,如下图所示。
|
||||
|
||||

|
||||
|
||||
原神启动完成后,点击进入游戏即可。
|
||||
|
||||
点击进入游戏后原神自动退出,工具会提示您选择导出至何种工具,如下图所示。
|
||||
|
||||

|
||||
|
||||
此时可根据自己的需要进行选择,一般推荐导出至[0]椰羊以及[4]表格文件(.csv)。
|
||||
|
||||
选择完毕后各工具导出页面如下:
|
||||
|
||||
#### 椰羊:
|
||||
|
||||

|
||||
|
||||
#### Snap.Hutao:
|
||||
|
||||

|
||||
|
||||
#### Seelie.me:
|
||||
|
||||
此时YaeAchievement会提示成就数据已导出。请在保存YaeAchievement的文件夹内找到名称形如export-20xxxxxxxxxxxx-seelie.json的文件。
|
||||
|
||||

|
||||
|
||||
然后点击该网址https://seelie.me/settings, 进入网页后选择导入,如下图所示。
|
||||
|
||||

|
||||
|
||||
点击导入后选中名称形如export-20xxxxxxxxxxxx-seelie.json的文件,如下图所示。
|
||||
|
||||

|
||||
|
||||
当弹出如下图所示的提示时表示导入成功。
|
||||
|
||||

|
||||
|
||||
此时可选择左栏成就,查看导入的成就数据。
|
||||
|
||||
#### 寻空:
|
||||
|
||||

|
||||
|
||||
### 各种工具的介绍烦请移步至各工具的官方页面进行查看(下方序号对应导出序号)
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. [表格文件 `.csv`](https://en.wikipedia.org/wiki/Comma-separated_values)
|
||||
5. [寻空](https://github.com/xunkong/xunkong)
|
||||
6. [原魔工具箱](https://apps.apple.com/app/id1663989619)
|
||||
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON 文件
|
||||
62
Tutorial_EN.md
Normal file
62
Tutorial_EN.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Instructions for Use
|
||||
|
||||
1.Download YaeAchievement(Latest Version):
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
For global user, you should select [3] Seelie.me or [4] Export to csv file。
|
||||
|
||||
After selecting, each page exports the tool as follows:
|
||||
|
||||
#### Snap.Hutao:
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||

|
||||
|
||||
Then click on the URL: https://seelie.me/settings, enter the website and select Import Account, as illustrated in the figure below.
|
||||
|
||||

|
||||
|
||||
After clicking Import, select a file named export-20xxxxxxxxxxxx-seelie.json, as shown in the figure below.
|
||||
|
||||

|
||||
|
||||
When the prompt as shown in the image below pops up, the import process succeeds.
|
||||
|
||||

|
||||
|
||||
At this time, you can select the Achievements in the left column to view the imported performance data.
|
||||
|
||||
### For the introduction of different tools, please visit the official page of each tool to see:
|
||||
|
||||
0. [Cocogoat](https://cocogoat.work/achievement)
|
||||
1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. [Form File `.csv`](https://en.wikipedia.org/wiki/Comma-separated_values)
|
||||
5. [XunKong](https://github.com/xunkong/xunkong)
|
||||
6. [YuanmoTools](https://apps.apple.com/app/id1663989619)
|
||||
7. [Teyvat Guide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON file
|
||||
61
Tutorial_JP.md
Normal file
61
Tutorial_JP.md
Normal file
@@ -0,0 +1,61 @@
|
||||
## 使用説明書
|
||||
|
||||
1. YaeAchievement(最新バージョン)をダウンロード:
|
||||
|
||||
こちらをクリック:https://github.com/HolographicHat/YaeAchievement/releases
|
||||
|
||||
赤枠で囲まれた「YaeAchievement.exe」という名前のファイルをクリックすると、自動的にポップアップしてダウンロードされます。このファイルをデスクトップや他の見やすいフォルダに保存することをお勧めします。
|
||||
|
||||

|
||||
|
||||
3. メインプログラムを開くための操作と実績エクスポートのオプション
|
||||
|
||||
最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。
|
||||
|
||||

|
||||
|
||||
原神の起動が完了したら、ゲームに入ります。
|
||||
|
||||
ゲームに入ると、ツールがどのツールにエクスポートするかを選択するように促します。以下の図のように表示されます。
|
||||
|
||||

|
||||
|
||||
グローバルユーザーの場合、[3] Seelie.meまたは[4] csvファイルにエクスポートを選択する必要があります。
|
||||
|
||||
選択後、各ページは次のようにツールをエクスポートします:
|
||||
|
||||
#### Snap.Hutao:
|
||||
|
||||

|
||||
|
||||
#### Seelie.me:
|
||||
|
||||
この時点で、Yae Achievementは実績データがエクスポートされたことを通知します。Yae Achievement保存ディレクトリにexport-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを見つけてください。
|
||||
|
||||

|
||||
|
||||
次に、URL:https://seelie.me/settings をクリックし、ウェブサイトにアクセスしてインポートアカウントを選択します。以下の図のように表示されます。
|
||||
|
||||

|
||||
|
||||
インポートをクリックした後、export-20xxxxxxxxxxxx-seelie.jsonという名前のファイルを選択します。以下の図のように表示されます。
|
||||
|
||||

|
||||
|
||||
以下の図のようなプロンプトが表示されたら、インポートプロセスは成功です。
|
||||
|
||||

|
||||
|
||||
この時点で、左側の列のAchievementsを選択して、インポートされた実績データを表示できます。
|
||||
|
||||
### 各ツールの紹介については、各ツールの公式ページをご覧ください:
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. [フォームファイル `.csv`](https://ja.wikipedia.org/wiki/Comma-separated_values)
|
||||
5. [尋空](https://github.com/xunkong/xunkong)
|
||||
6. [原魔ツールボックス](https://apps.apple.com/app/id1663989619)
|
||||
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSONファイル
|
||||
104
YaeAchievement.SourceGeneration/MinHookAttachGenerator.cs
Normal file
104
YaeAchievement.SourceGeneration/MinHookAttachGenerator.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
5
YaeAchievement.slnx
Normal file
5
YaeAchievement.slnx
Normal file
@@ -0,0 +1,5 @@
|
||||
<Solution>
|
||||
<Project Path="YaeAchievement.SourceGeneration/YaeAchievement.SourceGeneration.csproj" />
|
||||
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" />
|
||||
<Project Path="YaeAchievement\YaeAchievement.csproj" />
|
||||
</Solution>
|
||||
6
YaeAchievement/NativeMethods.json
Normal file
6
YaeAchievement/NativeMethods.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"className": "Native",
|
||||
"allowMarshaling": false,
|
||||
"public": false
|
||||
}
|
||||
30
YaeAchievement/NativeMethods.txt
Normal file
30
YaeAchievement/NativeMethods.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
// kernel32
|
||||
GlobalLock
|
||||
OpenProcess
|
||||
GetStdHandle
|
||||
GlobalUnlock
|
||||
ResumeThread
|
||||
Module32Next
|
||||
Module32First
|
||||
CreateProcess
|
||||
LoadLibraryEx
|
||||
VirtualFreeEx
|
||||
VirtualAllocEx
|
||||
GetProcAddress
|
||||
GetConsoleMode
|
||||
SetConsoleMode
|
||||
TerminateProcess
|
||||
CreateRemoteThread
|
||||
WriteProcessMemory
|
||||
WaitForSingleObject
|
||||
GetCurrentConsoleFontEx
|
||||
CreateToolhelp32Snapshot
|
||||
|
||||
// psapi
|
||||
GetModuleFileNameEx
|
||||
|
||||
// user32
|
||||
OpenClipboard
|
||||
CloseClipboard
|
||||
EmptyClipboard
|
||||
SetClipboardData
|
||||
66
YaeAchievement/YaeAchievement.csproj
Normal file
66
YaeAchievement/YaeAchievement.csproj
Normal 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>
|
||||
558
YaeAchievement/res/App.Designer.cs
generated
Normal file
558
YaeAchievement/res/App.Designer.cs
generated
Normal file
@@ -0,0 +1,558 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YaeAchievement.res {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class App {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal App() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("YaeAchievement.res.App", typeof(App).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to all achievement.
|
||||
/// </summary>
|
||||
internal static string AllAchievement {
|
||||
get {
|
||||
return ResourceManager.GetString("AllAchievement", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please close another instance..
|
||||
/// </summary>
|
||||
internal static string AnotherInstance {
|
||||
get {
|
||||
return ResourceManager.GetString("AnotherInstance", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to YaeAchievement ({0}).
|
||||
/// </summary>
|
||||
internal static string AppBanner {
|
||||
get {
|
||||
return ResourceManager.GetString("AppBanner", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No.
|
||||
/// </summary>
|
||||
internal static string CommonNo {
|
||||
get {
|
||||
return ResourceManager.GetString("CommonNo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Yes.
|
||||
/// </summary>
|
||||
internal static string CommonYes {
|
||||
get {
|
||||
return ResourceManager.GetString("CommonYes", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please launch GenshinImpact to continue..
|
||||
/// </summary>
|
||||
internal static string ConfigNeedStartGenshin {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfigNeedStartGenshin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Download: {0}.
|
||||
/// </summary>
|
||||
internal static string DownloadLink {
|
||||
get {
|
||||
return ResourceManager.GetString("DownloadLink", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Network error ({0}: {1}).
|
||||
/// </summary>
|
||||
internal static string ExceptionNetwork {
|
||||
get {
|
||||
return ResourceManager.GetString("ExceptionNetwork", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Export to:.
|
||||
/// </summary>
|
||||
internal static string ExportChoose {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportChoose", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cocogoat (https://cocogoat.work/achievement).
|
||||
/// </summary>
|
||||
internal static string ExportTargetCocogoat {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetCocogoat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Csv file.
|
||||
/// </summary>
|
||||
internal static string ExportTargetCsv {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetCsv", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Snap.HuTao.
|
||||
/// </summary>
|
||||
internal static string ExportTargetHuTao {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetHuTao", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Paimon.moe.
|
||||
/// </summary>
|
||||
internal static string ExportTargetPaimon {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetPaimon", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Seelie.me.
|
||||
/// </summary>
|
||||
internal static string ExportTargetSeelie {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetSeelie", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Teyvat Guide.
|
||||
/// </summary>
|
||||
internal static string ExportTargetTeyvatGuide {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetTeyvatGuide", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to UIAF JSON File.
|
||||
/// </summary>
|
||||
internal static string ExportTargetUIAFJson {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetUIAFJson", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to .
|
||||
/// </summary>
|
||||
internal static string ExportTargetWxApp1 {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetWxApp1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Xunkong.
|
||||
/// </summary>
|
||||
internal static string ExportTargetXunkong {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetXunkong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fail, please contact developer to get help information (CG_{0}).
|
||||
/// </summary>
|
||||
internal static string ExportToCocogoatFail {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToCocogoatFail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Successfully exported to cocogoat..
|
||||
/// </summary>
|
||||
internal static string ExportToCocogoatSuccess {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToCocogoatSuccess", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Successfully exported to {0}.
|
||||
/// </summary>
|
||||
internal static string ExportToFileSuccess {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToFileSuccess", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please update Snap Hutao and retry..
|
||||
/// </summary>
|
||||
internal static string ExportToSnapGenshinNeedUpdate {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToSnapGenshinNeedUpdate", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Successfully exported to Snap Hutao..
|
||||
/// </summary>
|
||||
internal static string ExportToSnapGenshinSuccess {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToSnapGenshinSuccess", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please launch/update Teyvat Guide and retry..
|
||||
/// </summary>
|
||||
internal static string ExportToTauriFail {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToTauriFail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Successfully exported to Teyvat Guide..
|
||||
/// </summary>
|
||||
internal static string ExportToTauriSuccess {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToTauriSuccess", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0}.
|
||||
/// </summary>
|
||||
internal static string ExportToWxApp1Success {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToWxApp1Success", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please update xunkong and retry..
|
||||
/// </summary>
|
||||
internal static string ExportToXunkongNeedUpdate {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToXunkongNeedUpdate", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Successfully exported to xunkong..
|
||||
/// </summary>
|
||||
internal static string ExportToXunkongSuccess {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportToXunkongSuccess", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Game process start ({0}).
|
||||
/// </summary>
|
||||
internal static string GameLoading {
|
||||
get {
|
||||
return ResourceManager.GetString("GameLoading", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Game exited..
|
||||
/// </summary>
|
||||
internal static string GameProcessExit {
|
||||
get {
|
||||
return ResourceManager.GetString("GameProcessExit", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please update genshin and retry..
|
||||
/// </summary>
|
||||
internal static string GenshinHashError {
|
||||
get {
|
||||
return ResourceManager.GetString("GenshinHashError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please close game before run this application. ({0}).
|
||||
/// </summary>
|
||||
internal static string GenshinIsRunning {
|
||||
get {
|
||||
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>
|
||||
/// Looks up a localized string similar to Network error:.
|
||||
/// </summary>
|
||||
internal static string NetworkError {
|
||||
get {
|
||||
return ResourceManager.GetString("NetworkError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No write permission on {0}..
|
||||
/// </summary>
|
||||
internal static string NoWritePermission {
|
||||
get {
|
||||
return ResourceManager.GetString("NoWritePermission", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Press any key to exit..
|
||||
/// </summary>
|
||||
internal static string PressKeyToExit {
|
||||
get {
|
||||
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>
|
||||
/// Looks up a localized string similar to Reward not taken.
|
||||
/// </summary>
|
||||
internal static string StatusFinished {
|
||||
get {
|
||||
return ResourceManager.GetString("StatusFinished", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Invalid.
|
||||
/// </summary>
|
||||
internal static string StatusInvalid {
|
||||
get {
|
||||
return ResourceManager.GetString("StatusInvalid", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Finished.
|
||||
/// </summary>
|
||||
internal static string StatusRewardTaken {
|
||||
get {
|
||||
return ResourceManager.GetString("StatusRewardTaken", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unfinished.
|
||||
/// </summary>
|
||||
internal static string StatusUnfinished {
|
||||
get {
|
||||
return ResourceManager.GetString("StatusUnfinished", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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}.
|
||||
/// </summary>
|
||||
internal static string UpdateDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Downloading update package....
|
||||
/// </summary>
|
||||
internal static string UpdateDownloading {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateDownloading", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The process cannot access the file '{0}' 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>
|
||||
/// Looks up a localized string similar to Has update: {0} => {1}.
|
||||
/// </summary>
|
||||
internal static string UpdateNewVersion {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateNewVersion", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Upload error to appcenter....
|
||||
/// </summary>
|
||||
internal static string UploadError {
|
||||
get {
|
||||
return ResourceManager.GetString("UploadError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use previous fetched data?.
|
||||
/// </summary>
|
||||
internal static string UsePreviousData {
|
||||
get {
|
||||
return ResourceManager.GetString("UsePreviousData", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Downloading Visual C++ Redistributable....
|
||||
/// </summary>
|
||||
internal static string VcRuntimeDownload {
|
||||
get {
|
||||
return ResourceManager.GetString("VcRuntimeDownload", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Installing Visual C++ Redistributable....
|
||||
/// </summary>
|
||||
internal static string VcRuntimeInstalling {
|
||||
get {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
187
YaeAchievement/res/App.resx
Normal file
187
YaeAchievement/res/App.resx
Normal file
@@ -0,0 +1,187 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ExportToCocogoatFail" xml:space="preserve">
|
||||
<value>Fail, please contact developer to get help information (CG_{0})</value>
|
||||
</data>
|
||||
<data name="AllAchievement" xml:space="preserve">
|
||||
<value>all achievement</value>
|
||||
</data>
|
||||
<data name="ExportChoose" xml:space="preserve">
|
||||
<value>Export to:</value>
|
||||
</data>
|
||||
<data name="ExportToCocogoatSuccess" xml:space="preserve">
|
||||
<value>Successfully exported to cocogoat.</value>
|
||||
</data>
|
||||
<data name="ExportToWxApp1Success" xml:space="preserve">
|
||||
<value>{0}</value>
|
||||
</data>
|
||||
<data name="ExportToSnapGenshinSuccess" xml:space="preserve">
|
||||
<value>Successfully exported to Snap Hutao.</value>
|
||||
</data>
|
||||
<data name="ExportToSnapGenshinNeedUpdate" xml:space="preserve">
|
||||
<value>Please update Snap Hutao and retry.</value>
|
||||
</data>
|
||||
<data name="ExportToFileSuccess" xml:space="preserve">
|
||||
<value>Successfully exported to {0}</value>
|
||||
</data>
|
||||
<data name="ExportToXunkongSuccess" xml:space="preserve">
|
||||
<value>Successfully exported to xunkong.</value>
|
||||
</data>
|
||||
<data name="ExportToXunkongNeedUpdate" xml:space="preserve">
|
||||
<value>Please update xunkong and retry.</value>
|
||||
</data>
|
||||
<data name="StatusInvalid" xml:space="preserve">
|
||||
<value>Invalid</value>
|
||||
</data>
|
||||
<data name="StatusRewardTaken" xml:space="preserve">
|
||||
<value>Finished</value>
|
||||
</data>
|
||||
<data name="StatusUnfinished" xml:space="preserve">
|
||||
<value>Unfinished</value>
|
||||
</data>
|
||||
<data name="StatusFinished" xml:space="preserve">
|
||||
<value>Reward not taken</value>
|
||||
</data>
|
||||
<data name="ConfigNeedStartGenshin" xml:space="preserve">
|
||||
<value>Please launch GenshinImpact to continue.</value>
|
||||
</data>
|
||||
<data name="DownloadLink" xml:space="preserve">
|
||||
<value>Download: {0}</value>
|
||||
</data>
|
||||
<data name="GameProcessExit" xml:space="preserve">
|
||||
<value>Game exited.</value>
|
||||
</data>
|
||||
<data name="GameLoading" xml:space="preserve">
|
||||
<value>Game process start ({0})</value>
|
||||
</data>
|
||||
<data name="UploadError" xml:space="preserve">
|
||||
<value>Upload error to appcenter...</value>
|
||||
</data>
|
||||
<data name="PressKeyToExit" xml:space="preserve">
|
||||
<value>Press any key to exit.</value>
|
||||
</data>
|
||||
<data name="GenshinIsRunning" xml:space="preserve">
|
||||
<value>Please close game before run this application. ({0})</value>
|
||||
</data>
|
||||
<data name="AnotherInstance" xml:space="preserve">
|
||||
<value>Please close another instance.</value>
|
||||
</data>
|
||||
<data name="UpdateNewVersion" xml:space="preserve">
|
||||
<value>Has update: {0} => {1}</value>
|
||||
</data>
|
||||
<data name="UpdateDescription" xml:space="preserve">
|
||||
<value>Description:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="UpdateDownloading" xml:space="preserve">
|
||||
<value>Downloading update package...</value>
|
||||
</data>
|
||||
<data name="AppBanner" xml:space="preserve">
|
||||
<value>YaeAchievement ({0})</value>
|
||||
</data>
|
||||
<data name="UsePreviousData" xml:space="preserve">
|
||||
<value>Use previous fetched data?</value>
|
||||
</data>
|
||||
<data name="NetworkError" xml:space="preserve">
|
||||
<value>Network error:</value>
|
||||
</data>
|
||||
<data name="VcRuntimeDownload" xml:space="preserve">
|
||||
<value>Downloading Visual C++ Redistributable...</value>
|
||||
</data>
|
||||
<data name="VcRuntimeInstalling" xml:space="preserve">
|
||||
<value>Installing Visual C++ Redistributable...</value>
|
||||
</data>
|
||||
<data name="ExceptionNetwork" xml:space="preserve">
|
||||
<value>Network error ({0}: {1})</value>
|
||||
</data>
|
||||
<data name="GenshinHashError" xml:space="preserve">
|
||||
<value>Please update genshin and retry.</value>
|
||||
</data>
|
||||
<data name="NoWritePermission" xml:space="preserve">
|
||||
<value>No write permission on {0}.</value>
|
||||
</data>
|
||||
<data name="ExportToTauriSuccess" xml:space="preserve">
|
||||
<value>Successfully exported to Teyvat Guide.</value>
|
||||
</data>
|
||||
<data name="ExportToTauriFail" xml:space="preserve">
|
||||
<value>Please launch/update Teyvat Guide and retry.</value>
|
||||
</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>
|
||||
180
YaeAchievement/res/App.zh.resx
Normal file
180
YaeAchievement/res/App.zh.resx
Normal file
@@ -0,0 +1,180 @@
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ExportToCocogoatFail" xml:space="preserve">
|
||||
<value>导出失败, 请联系开发者以获取帮助(CG_{0})</value>
|
||||
</data>
|
||||
<data name="AllAchievement" xml:space="preserve">
|
||||
<value>全部成就</value>
|
||||
</data>
|
||||
<data name="ExportChoose" xml:space="preserve">
|
||||
<value>要导出到哪里?</value>
|
||||
</data>
|
||||
<data name="ExportToCocogoatSuccess" xml:space="preserve">
|
||||
<value>在浏览器内进行下一步操作</value>
|
||||
</data>
|
||||
<data name="ExportToWxApp1Success" xml:space="preserve">
|
||||
<value>在小程序导入页面输入以下代码: {0}</value>
|
||||
</data>
|
||||
<data name="ExportToSnapGenshinSuccess" xml:space="preserve">
|
||||
<value>在 Snap Hutao 中进行下一步操作</value>
|
||||
</data>
|
||||
<data name="ExportToSnapGenshinNeedUpdate" xml:space="preserve">
|
||||
<value>更新/安装 Snap Hutao 最新版本后重试</value>
|
||||
</data>
|
||||
<data name="ExportToFileSuccess" xml:space="preserve">
|
||||
<value>成就数据已导出至 {0}</value>
|
||||
</data>
|
||||
<data name="ExportToXunkongSuccess" xml:space="preserve">
|
||||
<value>在寻空中进行下一步操作</value>
|
||||
</data>
|
||||
<data name="ExportToXunkongNeedUpdate" xml:space="preserve">
|
||||
<value>更新寻空至最新版本后重试</value>
|
||||
</data>
|
||||
<data name="StatusInvalid" xml:space="preserve">
|
||||
<value>未知</value>
|
||||
</data>
|
||||
<data name="StatusFinished" xml:space="preserve">
|
||||
<value>已完成但未领取奖励</value>
|
||||
</data>
|
||||
<data name="StatusUnfinished" xml:space="preserve">
|
||||
<value>未完成</value>
|
||||
</data>
|
||||
<data name="StatusRewardTaken" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
<data name="ConfigNeedStartGenshin" xml:space="preserve">
|
||||
<value>请打开 原神 后继续操作</value>
|
||||
</data>
|
||||
<data name="DownloadLink" xml:space="preserve">
|
||||
<value>下载地址: {0}</value>
|
||||
</data>
|
||||
<data name="GameProcessExit" xml:space="preserve">
|
||||
<value>游戏进程异常退出</value>
|
||||
</data>
|
||||
<data name="GameLoading" xml:space="preserve">
|
||||
<value>原神正在启动 ({0})</value>
|
||||
</data>
|
||||
<data name="UploadError" xml:space="preserve">
|
||||
<value>正在上报错误信息...</value>
|
||||
</data>
|
||||
<data name="PressKeyToExit" xml:space="preserve">
|
||||
<value>按任意键退出</value>
|
||||
</data>
|
||||
<data name="GenshinIsRunning" xml:space="preserve">
|
||||
<value>原神正在运行,请关闭后重试 ({0})</value>
|
||||
</data>
|
||||
<data name="AnotherInstance" xml:space="preserve">
|
||||
<value>另一个实例正在运行,请关闭后重试</value>
|
||||
</data>
|
||||
<data name="UpdateNewVersion" xml:space="preserve">
|
||||
<value>有可用更新: {0} => {1}</value>
|
||||
</data>
|
||||
<data name="UpdateDescription" xml:space="preserve">
|
||||
<value>更新内容:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="UpdateDownloading" xml:space="preserve">
|
||||
<value>正在下载更新包...</value>
|
||||
</data>
|
||||
<data name="AppBanner" xml:space="preserve">
|
||||
<value>YaeAchievement - 原神成就导出工具 ({0})</value>
|
||||
</data>
|
||||
<data name="UsePreviousData" xml:space="preserve">
|
||||
<value>要使用上一次获取到的成就数据吗?</value>
|
||||
</data>
|
||||
<data name="NetworkError" xml:space="preserve">
|
||||
<value>网络错误: {0}</value>
|
||||
</data>
|
||||
<data name="VcRuntimeDownload" xml:space="preserve">
|
||||
<value>正在下载 Visual C++ Redistributable...</value>
|
||||
</data>
|
||||
<data name="VcRuntimeInstalling" xml:space="preserve">
|
||||
<value>正在安装 Visual C++ Redistributable...</value>
|
||||
</data>
|
||||
<data name="ExceptionNetwork" xml:space="preserve">
|
||||
<value>网络错误,请检查网络后重试 ({0}: {1})</value>
|
||||
</data>
|
||||
<data name="GenshinHashError" xml:space="preserve">
|
||||
<value>当前适配版本不匹配,请更新原神至最新版本后重试或等待工具更新。</value>
|
||||
</data>
|
||||
<data name="NoWritePermission" xml:space="preserve">
|
||||
<value>无法写入文件,请更换软件所在目录后重试</value>
|
||||
</data>
|
||||
<data name="ExportToTauriFail" xml:space="preserve">
|
||||
<value>启动 Teyvat Guide 或更新 Teyvat Guide 至最新版本后重试</value>
|
||||
</data>
|
||||
<data name="ExportToTauriSuccess" xml:space="preserve">
|
||||
<value>在 Teyvat Guide 进行下一步操作</value>
|
||||
</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>
|
||||
BIN
YaeAchievement/res/Updater.exe
Normal file
BIN
YaeAchievement/res/Updater.exe
Normal file
Binary file not shown.
78
YaeAchievement/res/app.manifest
Normal file
78
YaeAchievement/res/app.manifest
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- UAC 清单选项
|
||||
如果想要更改 Windows 用户帐户控制级别,请使用
|
||||
以下节点之一替换 requestedExecutionLevel 节点。
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
|
||||
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
|
||||
元素。
|
||||
-->
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
|
||||
Windows 版本的列表。取消评论适当的元素,
|
||||
Windows 将自动选择最兼容的环境。 -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
|
||||
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
|
||||
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
|
||||
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
|
||||
|
||||
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
|
||||
<!--
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</assembly>
|
||||
BIN
YaeAchievement/res/icon.ico
Normal file
BIN
YaeAchievement/res/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
42
YaeAchievement/res/proto/AchievementInfo.proto
Normal file
42
YaeAchievement/res/proto/AchievementInfo.proto
Normal 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;
|
||||
}
|
||||
10
YaeAchievement/res/proto/CacheItem.proto
Normal file
10
YaeAchievement/res/proto/CacheItem.proto
Normal file
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Proto";
|
||||
|
||||
message CacheItem {
|
||||
uint32 version = 1;
|
||||
string checksum = 2;
|
||||
string etag = 3;
|
||||
bytes content = 4;
|
||||
}
|
||||
79
YaeAchievement/res/proto/StoreData.proto
Normal file
79
YaeAchievement/res/proto/StoreData.proto
Normal 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;
|
||||
}
|
||||
}
|
||||
20
YaeAchievement/res/proto/UpdateInfo.proto
Normal file
20
YaeAchievement/res/proto/UpdateInfo.proto
Normal 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;
|
||||
}
|
||||
90
YaeAchievement/src/AppConfig.cs
Normal file
90
YaeAchievement/src/AppConfig.cs
Normal 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();
|
||||
|
||||
}
|
||||
237
YaeAchievement/src/Export.cs
Normal file
237
YaeAchievement/src/Export.cs
Normal 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;
|
||||
35
YaeAchievement/src/GlobalVars.cs
Normal file
35
YaeAchievement/src/GlobalVars.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
global using System.Diagnostics;
|
||||
global using YaeAchievement.res;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Proto;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class GlobalVars {
|
||||
|
||||
public static bool PauseOnExit { get; set; } = true;
|
||||
public static Version AppVersion { get; } = Assembly.GetEntryAssembly()!.GetName().Version!;
|
||||
|
||||
public static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
private static readonly string CommonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
public static readonly string DataPath = Path.Combine(CommonData, "Yae");
|
||||
public static readonly string CachePath = Path.Combine(DataPath, "cache");
|
||||
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
|
||||
|
||||
public const uint AppVersionCode = 242;
|
||||
public const string AppVersionName = "5.7.2";
|
||||
|
||||
public const string PipeName = "YaeAchievementPipe";
|
||||
|
||||
[field:MaybeNull]
|
||||
public static AchievementInfo AchievementInfo =>
|
||||
field ??= AchievementInfo.Parser.ParseFrom(Utils.GetBucketFile("schicksal/metadata").GetAwaiter().GetResult());
|
||||
|
||||
static GlobalVars() {
|
||||
Directory.CreateDirectory(DataPath);
|
||||
Directory.CreateDirectory(CachePath);
|
||||
}
|
||||
|
||||
}
|
||||
37
YaeAchievement/src/Outputs/Paimon.cs
Normal file
37
YaeAchievement/src/Outputs/Paimon.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
39
YaeAchievement/src/Outputs/Seelie.cs
Normal file
39
YaeAchievement/src/Outputs/Seelie.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
65
YaeAchievement/src/Outputs/UIAF.cs
Normal file
65
YaeAchievement/src/Outputs/UIAF.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
151
YaeAchievement/src/Parsers/AchievementAllDataNotify.cs
Normal file
151
YaeAchievement/src/Parsers/AchievementAllDataNotify.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
106
YaeAchievement/src/Parsers/PlayerPropNotify.cs
Normal file
106
YaeAchievement/src/Parsers/PlayerPropNotify.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
61
YaeAchievement/src/Parsers/PlayerStoreNotify.cs
Normal file
61
YaeAchievement/src/Parsers/PlayerStoreNotify.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
95
YaeAchievement/src/Program.cs
Normal file
95
YaeAchievement/src/Program.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
53
YaeAchievement/src/Utilities/CacheFile.cs
Normal file
53
YaeAchievement/src/Utilities/CacheFile.cs
Normal 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));
|
||||
|
||||
}
|
||||
26
YaeAchievement/src/Utilities/Crc32.cs
Normal file
26
YaeAchievement/src/Utilities/Crc32.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
31
YaeAchievement/src/Utilities/Extensions/Enumerable.cs
Normal file
31
YaeAchievement/src/Utilities/Extensions/Enumerable.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
24
YaeAchievement/src/Utilities/Extensions/Process.cs
Normal file
24
YaeAchievement/src/Utilities/Extensions/Process.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
38
YaeAchievement/src/Utilities/Extensions/Stream.cs
Normal file
38
YaeAchievement/src/Utilities/Extensions/Stream.cs
Normal 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()));
|
||||
}
|
||||
|
||||
}
|
||||
127
YaeAchievement/src/Utilities/GameProcess.cs
Normal file
127
YaeAchievement/src/Utilities/GameProcess.cs
Normal 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);
|
||||
|
||||
}
|
||||
47
YaeAchievement/src/Utilities/SelectionPromptCompat.cs
Normal file
47
YaeAchievement/src/Utilities/SelectionPromptCompat.cs
Normal 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
355
YaeAchievement/src/Utils.cs
Normal 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!;
|
||||
}
|
||||
}
|
||||
71
YaeAchievementLib/YaeAchievementLib.csproj
Normal file
71
YaeAchievementLib/YaeAchievementLib.csproj
Normal 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>
|
||||
BIN
YaeAchievementLib/lib/libMinHook.x64.lib
Normal file
BIN
YaeAchievementLib/lib/libMinHook.x64.lib
Normal file
Binary file not shown.
166
YaeAchievementLib/src/Application.cs
Normal file
166
YaeAchievementLib/src/Application.cs
Normal 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
|
||||
|
||||
}
|
||||
109
YaeAchievementLib/src/Goshujin.cs
Normal file
109
YaeAchievementLib/src/Goshujin.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
YaeAchievementLib/src/Utilities/Log.cs
Normal file
111
YaeAchievementLib/src/Utilities/Log.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
181
YaeAchievementLib/src/Utilities/Native.cs
Normal file
181
YaeAchievementLib/src/Utilities/Native.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
74
YaeAchievementLib/src/Utilities/WinApi.cs
Normal file
74
YaeAchievementLib/src/Utilities/WinApi.cs
Normal 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);
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
180
app.js
180
app.js
@@ -1,180 +0,0 @@
|
||||
const proxy = require("udp-proxy")
|
||||
const cp = require("child_process")
|
||||
const rs = require("./regionServer")
|
||||
const appcenter = require("./appcenter")
|
||||
const {
|
||||
initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkCDN, checkUpdate,
|
||||
brotliCompressSync, brotliDecompressSync, checkGameIsRunning, checkPortIsUsing
|
||||
} = require("./utils")
|
||||
const { exportData } = require("./export")
|
||||
const { enablePrivilege } = require("./native")
|
||||
|
||||
const onExit = () => {
|
||||
setupHost(true)
|
||||
console.log("按任意键退出")
|
||||
cp.execSync("pause > nul", { stdio: "inherit" })
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
process.once("SIGHUP", () => setupHost(true))
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.log("Unhandled Rejection at: ", promise, "\n0Reason:", reason)
|
||||
})
|
||||
process.once("uncaughtException", (err, origin) => {
|
||||
appcenter.uploadError(err, true)
|
||||
console.log(err)
|
||||
console.log(`Origin: ${origin}`)
|
||||
process.exit(1)
|
||||
})
|
||||
process.once("exit", onExit)
|
||||
process.once("SIGINT", onExit)
|
||||
try {
|
||||
enablePrivilege()
|
||||
} catch (e) {
|
||||
console.log("请使用管理员身份运行此程序")
|
||||
process.exit(-1)
|
||||
}
|
||||
appcenter.startup()
|
||||
let conf = await initConfig()
|
||||
checkPortIsUsing()
|
||||
checkGameIsRunning()
|
||||
log("检查更新")
|
||||
await checkUpdate()
|
||||
checkCDN().then(_ => debug("CDN check success.")).catch(reason => {
|
||||
console.log(reason)
|
||||
process.exit(113)
|
||||
})
|
||||
let gameProcess
|
||||
let unexpectedExit = true
|
||||
rs.create(conf,() => {
|
||||
setupHost()
|
||||
gameProcess = cp.execFile(conf.executable, { cwd: conf.path },err => {
|
||||
if (err !== null && !err.killed) {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
log("启动原神")
|
||||
gameProcess.on("exit", () => {
|
||||
if (unexpectedExit) {
|
||||
console.log("游戏进程异常退出")
|
||||
process.exit(0)
|
||||
}
|
||||
})
|
||||
},(ip, port, hServer) => {
|
||||
let login = false
|
||||
let cache = new Map()
|
||||
let lastRecvTimestamp = 0
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
const options = {
|
||||
address: ip,
|
||||
port: port,
|
||||
localaddress: "127.0.0.1",
|
||||
localport: 45678,
|
||||
middleware: {
|
||||
message: (msg, sender, next) => {
|
||||
const buf = Buffer.from(msg)
|
||||
if (!(login && buf.readUInt8(8) === 0x51)) {
|
||||
next(msg, sender)
|
||||
}
|
||||
},
|
||||
proxyMsg: (msg, sender, peer, next) => {
|
||||
try { next(msg, sender, peer) } catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let monitor;
|
||||
let stopped = false
|
||||
const createMonitor = () => {
|
||||
monitor = setInterval(async () => {
|
||||
if (login && lastRecvTimestamp + 2 < parseInt(Date.now() / 1000) && !stopped) {
|
||||
stopped = true
|
||||
unexpectedExit = false
|
||||
server.close(() => {})
|
||||
hServer.close()
|
||||
gameProcess.kill()
|
||||
clearInterval(monitor)
|
||||
setupHost(true)
|
||||
log("正在处理数据,请稍后...")
|
||||
let packets = Array.from(cache.values())
|
||||
cache.clear()
|
||||
packets.sort((a, b) => a.frg - b.frg)
|
||||
.sort((a, b) => a.sn - b.sn)
|
||||
.filter(i => i.data.byteLength !== 0)
|
||||
.forEach(i => {
|
||||
const psn = i.sn + i.frg
|
||||
cache.has(psn) ? (() => {
|
||||
const arr = cache.get(psn)
|
||||
arr.push(i.data)
|
||||
cache.set(psn, arr)
|
||||
})() : cache.set(psn, [i.data])
|
||||
})
|
||||
packets = Array.from(cache.values())
|
||||
.map(arr => {
|
||||
const data = Buffer.concat(arr)
|
||||
const len = Buffer.alloc(4)
|
||||
len.writeUInt32LE(data.length)
|
||||
return Buffer.concat([len, data])
|
||||
})
|
||||
const merged = Buffer.concat(packets)
|
||||
const compressed = brotliCompressSync(merged)
|
||||
const response = await upload(compressed)
|
||||
const data = brotliDecompressSync(response.data)
|
||||
if (response.status !== 200) {
|
||||
log(`发生错误: ${data}`)
|
||||
log(`请求ID: ${response.headers["x-api-requestid"]}`)
|
||||
log("请联系开发者以获取帮助")
|
||||
} else {
|
||||
const proto = await decodeProto(data,"AllAchievement")
|
||||
await exportData(proto)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
},1000)
|
||||
}
|
||||
const server = proxy.createServer(options)
|
||||
server.on("message", (msg, _) => {
|
||||
if (msg.byteLength > 500) {
|
||||
login = true
|
||||
}
|
||||
})
|
||||
server.on("error", err => console.log(`Proxy error: ${err.message}` + err.message))
|
||||
server.on("proxyError", err => console.log(`Proxy error: ${err.message}` + err.message))
|
||||
server.on("proxyMsg", (msg, _) => {
|
||||
lastRecvTimestamp = parseInt(Date.now() / 1000)
|
||||
let buf = Buffer.from(msg)
|
||||
if (buf.byteLength <= 20) {
|
||||
switch(buf.readUInt32BE(0)) {
|
||||
case 325:
|
||||
createMonitor()
|
||||
debug("服务端握手应答")
|
||||
break
|
||||
case 404:
|
||||
lastRecvTimestamp = parseInt(Date.now() / 1000) - 2333
|
||||
break
|
||||
default:
|
||||
console.log(`Unhandled: ${buf.toString("hex")}`)
|
||||
process.exit(2)
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
splitPacket(buf).forEach(sb => {
|
||||
if (sb.readUInt8(8) === 0x51) {
|
||||
const p = new KPacket(sb)
|
||||
if (!cache.has(p.hash)) cache.set(p.hash, p)
|
||||
}
|
||||
})
|
||||
})
|
||||
return server
|
||||
}).then(() => log("加载完毕"))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (e instanceof Error) {
|
||||
appcenter.uploadError(e, true)
|
||||
} else {
|
||||
appcenter.uploadError(Error(e), true)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
101
appcenter.js
101
appcenter.js
@@ -1,101 +0,0 @@
|
||||
const axios = require("axios")
|
||||
const crypto = require("crypto")
|
||||
const { version } = require("./version")
|
||||
const { getDeviceID, getDeviceInfo } = require("./native")
|
||||
|
||||
const getTimestamp = (d = new Date()) => {
|
||||
const p = i => i.toString().padStart(2, "0")
|
||||
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}.${p(d.getUTCMilliseconds())}Z`
|
||||
}
|
||||
|
||||
const queue = []
|
||||
const session = crypto.randomUUID()
|
||||
const key = "648b83bf-d439-49bd-97f4-e1e506bdfe39"
|
||||
|
||||
const install = (() => {
|
||||
const id = getDeviceID()
|
||||
return id === undefined ? crypto.randomUUID() : id
|
||||
})()
|
||||
|
||||
const device = (() => {
|
||||
const info = getDeviceInfo()
|
||||
info.appBuild = version.code
|
||||
info.appVersion = version.name
|
||||
info.sdkName = "appcenter.wpf.netcore"
|
||||
info.sdkVersion = "4.5.0"
|
||||
info.osName = "WINDOWS"
|
||||
info.appNamespace = "default"
|
||||
return info
|
||||
})()
|
||||
|
||||
const upload = () => {
|
||||
if (queue.length > 0) {
|
||||
const data = JSON.stringify({ "logs": queue })
|
||||
axios.post("https://in.appcenter.ms/logs?api-version=1.0.0", data,{
|
||||
headers: {
|
||||
"App-Secret": key,
|
||||
"Install-ID": install
|
||||
}
|
||||
}).then(_ => {
|
||||
queue.length = 0
|
||||
}).catch(_ => {})
|
||||
}
|
||||
}
|
||||
|
||||
const uploadError = (err, fatal) => {
|
||||
const eid = crypto.randomUUID()
|
||||
const reportJson = process.report.getReport(err)
|
||||
const reportAttachment = {
|
||||
type: "errorAttachment",
|
||||
device: device,
|
||||
timestamp: getTimestamp(),
|
||||
id: crypto.randomUUID(),
|
||||
sid: session,
|
||||
errorId: eid,
|
||||
contentType: "application/json",
|
||||
fileName: "report.json",
|
||||
data: Buffer.from(JSON.stringify(reportJson, null, 2), "utf-8").toString("base64")
|
||||
}
|
||||
// noinspection JSUnresolvedVariable
|
||||
const errorContent = {
|
||||
type: "managedError",
|
||||
id: eid,
|
||||
sid: session,
|
||||
architecture: "AMD64",
|
||||
userId: install,
|
||||
fatal: fatal,
|
||||
processId: process.pid,
|
||||
processName: process.argv0.replaceAll("\\", "/").split("/").pop(),
|
||||
timestamp: getTimestamp(),
|
||||
appLaunchTimestamp: getTimestamp(new Date(Date.now() - process.uptime())),
|
||||
exception: {
|
||||
"type": err.name,
|
||||
"message": err.message,
|
||||
"stackTrace": err.stack
|
||||
},
|
||||
device: device
|
||||
}
|
||||
queue.push(errorContent, reportAttachment)
|
||||
upload()
|
||||
}
|
||||
|
||||
const startup = () => {
|
||||
queue.push({
|
||||
type: "startService",
|
||||
services: [ "Analytics","Crashes" ],
|
||||
timestamp: getTimestamp(),
|
||||
device: device
|
||||
})
|
||||
queue.push({
|
||||
type: "startSession",
|
||||
sid: session,
|
||||
timestamp: getTimestamp(),
|
||||
device: device
|
||||
})
|
||||
upload()
|
||||
setInterval(() => upload(), 10000)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startup, upload, uploadError
|
||||
}
|
||||
141
export.js
141
export.js
@@ -1,141 +0,0 @@
|
||||
const fs = require("fs")
|
||||
const readline = require("readline")
|
||||
const { loadCache, log } = require("./utils")
|
||||
const { copyToClipboard } = require("./native")
|
||||
|
||||
const exportToSeelie = proto => {
|
||||
const out = { achievements: {} }
|
||||
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
|
||||
out.achievements[id] = { done: true }
|
||||
})
|
||||
const fp = `./export-${Date.now()}-seelie.json`
|
||||
fs.writeFileSync(fp, JSON.stringify(out))
|
||||
log(`导出为文件: ${fp}`)
|
||||
}
|
||||
|
||||
const exportToPaimon = async proto => {
|
||||
const out = { achievement: {} }
|
||||
const achTable = new Map()
|
||||
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
|
||||
excel.forEach(({GoalId, Id}) => {
|
||||
achTable.set(Id, GoalId === undefined ? 0 : GoalId)
|
||||
})
|
||||
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
|
||||
const gid = achTable.get(id)
|
||||
if (out.achievement[gid] === undefined) {
|
||||
out.achievement[gid] = {}
|
||||
}
|
||||
out.achievement[gid][id] = true
|
||||
})
|
||||
const fp = `./export-${Date.now()}-paimon.json`
|
||||
fs.writeFileSync(fp, JSON.stringify(out))
|
||||
log(`导出为文件: ${fp}`)
|
||||
}
|
||||
|
||||
const exportToCocogoat = async proto => {
|
||||
const out = {
|
||||
value: {
|
||||
achievements: []
|
||||
}
|
||||
}
|
||||
const achTable = new Map()
|
||||
const preStageAchievementIdList = []
|
||||
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
|
||||
excel.forEach(({GoalId, Id, PreStageAchievementId}) => {
|
||||
if (PreStageAchievementId !== undefined) {
|
||||
preStageAchievementIdList.push(PreStageAchievementId)
|
||||
}
|
||||
achTable.set(Id, GoalId === undefined ? 0 : GoalId)
|
||||
})
|
||||
const p = i => i.toString().padStart(2, "0")
|
||||
const getDate = ts => {
|
||||
const d = new Date(parseInt(`${ts}000`))
|
||||
return `${d.getFullYear()}/${p(d.getMonth()+1)}/${p(d.getDate())}`
|
||||
}
|
||||
proto.list.filter(achievement => achievement.status === 3).forEach(({current, finishTimestamp, id, require}) => {
|
||||
out.value.achievements.push({
|
||||
id: id,
|
||||
status: current === undefined || current === 0 || preStageAchievementIdList.includes(id) ? `${require}/${require}` : `${current}/${require}`,
|
||||
categoryId: achTable.get(id),
|
||||
date: getDate(finishTimestamp)
|
||||
})
|
||||
})
|
||||
const ts = Date.now()
|
||||
const json = JSON.stringify(out,null,2)
|
||||
copyToClipboard(json)
|
||||
const fp = `./export-${ts}-cocogoat.json`
|
||||
fs.writeFileSync(fp, json)
|
||||
log(`导出内容已复制到剪贴板`)
|
||||
}
|
||||
|
||||
const exportToCsv = async proto => {
|
||||
const excel = await loadCache("achievement-data.json", "HolographicHat/genshin-achievement-export")
|
||||
const achievementMap = new Map()
|
||||
excel["achievement"].forEach(obj => {
|
||||
achievementMap.set(parseInt(obj.id), obj)
|
||||
})
|
||||
const outputLines = ["ID,状态,特辑,名称,描述,当前进度,目标进度,完成时间"]
|
||||
const getStatusText = i => {
|
||||
switch (i) {
|
||||
case 1: return "未完成"
|
||||
case 2: return "已完成,未领取奖励"
|
||||
case 3: return "已完成"
|
||||
default: return "未知"
|
||||
}
|
||||
}
|
||||
const getTime = ts => {
|
||||
const d = new Date(parseInt(`${ts}000`))
|
||||
const p = i => i.toString().padStart(2, "0")
|
||||
return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
const bl = [84517]
|
||||
proto.list.forEach(({current, finishTimestamp, id, status, require}) => {
|
||||
if (!bl.includes(id)) {
|
||||
const desc = achievementMap.get(id) === undefined ? (() => {
|
||||
console.log(`Error get id ${id} in excel`)
|
||||
return {
|
||||
goal: "未知",
|
||||
name: "未知",
|
||||
desc: "未知"
|
||||
}
|
||||
})() : achievementMap.get(id)
|
||||
outputLines.push(`${id},${getStatusText(status)},${excel.goal[desc.goal]},${desc.name},${desc.desc},${status !== 1 ? current === 0 ? require : current : current},${require},${status === 1 ? "" : getTime(finishTimestamp)}`)
|
||||
}
|
||||
})
|
||||
const fp = `./export-${Date.now()}.csv`
|
||||
fs.writeFileSync(fp, `\uFEFF${outputLines.join("\n")}`)
|
||||
log(`导出为文件: ${fp}`)
|
||||
}
|
||||
|
||||
const exportData = async proto => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
const question = (query) => new Promise(resolve => {
|
||||
rl.question(query, resolve)
|
||||
})
|
||||
const chosen = await question("导出至: \n[0] 椰羊 (https://cocogoat.work/achievement)\n[1] Paimon.moe\n[2] Seelie.me\n[3] 表格文件 (默认)\n输入一个数字(0-3): ")
|
||||
rl.close()
|
||||
switch (chosen) {
|
||||
case "0":
|
||||
await exportToCocogoat(proto)
|
||||
break
|
||||
case "1":
|
||||
await exportToPaimon(proto)
|
||||
break
|
||||
case "2":
|
||||
await exportToSeelie(proto)
|
||||
break
|
||||
case "raw":
|
||||
fs.writeFileSync(`./export-${Date.now()}-raw.json`, JSON.stringify(proto,null,2))
|
||||
log("OK")
|
||||
break
|
||||
default:
|
||||
await exportToCsv(proto)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exportData
|
||||
}
|
||||
7
native.d.ts
vendored
7
native.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
export function selectGameExecutable(): string
|
||||
export function checkGameIsRunning(processName: string): boolean
|
||||
export function whoUseThePort(port: number): object
|
||||
export function copyToClipboard(value: string): any
|
||||
export function enablePrivilege(): any
|
||||
export function getDeviceInfo(): any
|
||||
export function getDeviceID(): string
|
||||
5
native/.gitignore
vendored
5
native/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
CMakeLists.txt
|
||||
.idea
|
||||
cmake-build-debug
|
||||
node_modules
|
||||
build
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "native",
|
||||
"sources": [
|
||||
"src/main.cc",
|
||||
"src/utils.h",
|
||||
"src/utils.cc",
|
||||
"src/define.h",
|
||||
"src/wmi/wmi.cpp",
|
||||
"src/wmi/wmi.hpp",
|
||||
"src/wmi/unistd.h",
|
||||
"src/wmi/wmiresult.cpp",
|
||||
"src/wmi/wmiresult.hpp",
|
||||
"src/wmi/wmiclasses.hpp",
|
||||
"src/wmi/wmiexception.hpp",
|
||||
"src/registry/registry.hpp"
|
||||
],
|
||||
"cflags!": [
|
||||
"-fno-exceptions"
|
||||
],
|
||||
"cflags_cc!": [
|
||||
"-fno-exceptions"
|
||||
],
|
||||
"defines": [
|
||||
"NAPI_DISABLE_CPP_EXCEPTIONS"
|
||||
],
|
||||
"include_dirs": [
|
||||
"<!(node -p \"require('node-addon-api').include_dir\")"
|
||||
],
|
||||
"msvs_settings": {
|
||||
"VCCLCompilerTool": {
|
||||
"AdditionalOptions": [
|
||||
"-std:c++latest",
|
||||
"-DUNICODE",
|
||||
"-sdl"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "genshin-export-native",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "node-gyp rebuild && copy .\\build\\Release\\native.node ..\\genshin-export\\"
|
||||
},
|
||||
"gypfile": true,
|
||||
"devDependencies": {
|
||||
"node-gyp": "^9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-addon-api": "^4.3.0"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <napi.h>
|
||||
#include <Windows.h>
|
||||
#include <TlHelp32.h>
|
||||
|
||||
using std::string, std::wstring, std::cout, std::to_string;
|
||||
using Napi::Object, Napi::Env, Napi::Function, Napi::Value, Napi::CallbackInfo, Napi::TypeError, Napi::Error;
|
||||
|
||||
typedef unsigned char byte;
|
||||
typedef unsigned long ul;
|
||||
typedef unsigned long long ull;
|
||||
@@ -1,177 +0,0 @@
|
||||
#include "utils.h"
|
||||
#include "define.h"
|
||||
#include "wmi/wmi.hpp"
|
||||
#include "wmi/wmiclasses.hpp"
|
||||
#include "registry/registry.hpp"
|
||||
#include <iphlpapi.h>
|
||||
#pragma comment(lib,"ws2_32.lib")
|
||||
#pragma comment(lib,"iphlpapi.lib")
|
||||
|
||||
using Wmi::Win32_ComputerSystem, Wmi::Win32_OperatingSystem, Wmi::retrieveWmi;
|
||||
|
||||
namespace native {
|
||||
|
||||
Value checkGameIsRunning(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
if (info.Length() != 1 || !info[0].IsString()) {
|
||||
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
wstring name = StringToWString(info[0].As<Napi::String>().Utf8Value());
|
||||
bool isRunning = false;
|
||||
PROCESSENTRY32 entry;
|
||||
entry.dwSize = sizeof(entry);
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
|
||||
if (Process32First(snapshot, &entry) == TRUE) {
|
||||
while (Process32Next(snapshot, &entry) == TRUE) {
|
||||
if (wstring(entry.szExeFile) == name) {
|
||||
isRunning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
CloseHandle(snapshot);
|
||||
return Napi::Boolean::New(env, isRunning);
|
||||
}
|
||||
|
||||
Value selectGameExecutable(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
Napi::String path;
|
||||
if (OpenFile(env, path) != ERROR_SUCCESS) {
|
||||
Error::New(env, "Failed to open file: " + to_string(CommDlgExtendedError())).ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Value whoUseThePort(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
if (info.Length() != 1 || !info[0].IsNumber()) {
|
||||
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
DWORD dwSize = 0;
|
||||
PMIB_TCPTABLE_OWNER_PID pTcpTable = nullptr;
|
||||
GetExtendedTcpTable(pTcpTable, &dwSize, TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0);
|
||||
pTcpTable = (PMIB_TCPTABLE_OWNER_PID)new byte[dwSize];
|
||||
if(GetExtendedTcpTable(pTcpTable,&dwSize,TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0) != NO_ERROR) {
|
||||
Error::New(env, "GetExtendedTcpTable failed").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
int port = info[0].As<Napi::Number>().Int32Value();
|
||||
auto nNum = (int)pTcpTable->dwNumEntries;
|
||||
DWORD pid = 0;
|
||||
for(int i = 0; i < nNum; i++) {
|
||||
if (htons(pTcpTable->table[i].dwLocalPort) == port) {
|
||||
pid = pTcpTable->table[i].dwOwningPid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
delete pTcpTable;
|
||||
Value ret = env.Undefined();
|
||||
if (pid != 0) {
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
|
||||
if (hProcess == nullptr) {
|
||||
Error::New(env, "OpenProcess error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
TCHAR fnBuf[MAX_PATH];
|
||||
DWORD length = MAX_PATH;
|
||||
if (QueryFullProcessImageName(hProcess, 0, fnBuf, &length) == 0) {
|
||||
Error::New(env, "QueryFullProcessImageName error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
Object obj = Object::New(env);
|
||||
obj.Set("pid", Napi::Number::New(env, pid));
|
||||
obj.Set("path", Napi::String::New(env, WStringToString(fnBuf)));
|
||||
ret = obj;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Value getDeviceID(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
wstring wd;
|
||||
if (RegUtils::GetString(HKEY_CURRENT_USER, L"SOFTWARE\\miHoYoSDK", L"MIHOYOSDK_DEVICE_ID", wd) != ERROR_SUCCESS) {
|
||||
return env.Undefined();
|
||||
}
|
||||
string id = WStringToString(wd);
|
||||
return Napi::String::New(env, id.substr(0, 8) + "-" + id.substr(8, 4) + "-" + id.substr(12, 4) + "-" + id.substr(16, 4) + "-" + id.substr(20, 12));
|
||||
}
|
||||
|
||||
Value getDeviceInfo(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
HDC desktop = GetDC(nullptr);
|
||||
int sw = GetDeviceCaps(desktop, DESKTOPHORZRES);
|
||||
int sh = GetDeviceCaps(desktop, DESKTOPVERTRES);
|
||||
ReleaseDC(nullptr, desktop);
|
||||
DWORD buildNum = RegUtils::GetInt(env, HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR");
|
||||
wstring locale;
|
||||
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International", L"LocaleName", locale) != ERROR_SUCCESS) {
|
||||
locale = L"zh-CN";
|
||||
}
|
||||
wstring country;
|
||||
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International\\Geo", L"Name", country) != ERROR_SUCCESS) {
|
||||
country = L"CN";
|
||||
}
|
||||
Object obj = Object::New(env);
|
||||
obj.Set("locale", Napi::String::New(env, WStringToString(locale)));
|
||||
obj.Set("screenSize", Napi::String::New(env, to_string(sw) + "x" + to_string(sh)));
|
||||
obj.Set("carrierCountry", Napi::String::New(env, WStringToString(country)));
|
||||
try {
|
||||
auto computer = retrieveWmi<Win32_ComputerSystem>();
|
||||
obj.Set("model", Napi::String::New(env, computer.Model));
|
||||
obj.Set("oemName", Napi::String::New(env, computer.Manufacturer));
|
||||
} catch (Wmi::WmiException &e) {
|
||||
obj.Set("model", Napi::String::New(env, "Unknown"));
|
||||
obj.Set("oemName", Napi::String::New(env, "Unknown"));
|
||||
}
|
||||
try {
|
||||
auto os = retrieveWmi<Win32_OperatingSystem>();
|
||||
string osv = os.Version;
|
||||
obj.Set("osBuild", Napi::String::New(env, osv + "." + to_string(buildNum)));
|
||||
obj.Set("osVersion", Napi::String::New(env, osv));
|
||||
obj.Set("timeZoneOffset", Napi::Number::New(env, os.CurrentTimeZone));
|
||||
} catch (Wmi::WmiException &e) {
|
||||
obj.Set("osBuild", Napi::String::New(env, "Unknown"));
|
||||
obj.Set("osVersion", Napi::String::New(env, "Unknown"));
|
||||
obj.Set("timeZoneOffset", Napi::Number::New(env, 480));
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
Value enablePrivilege(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
EnablePrivilege(env, L"SeDebugPrivilege");
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
Value copyToClipboard(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
string text = info[0].As<Napi::String>().Utf8Value();
|
||||
if (OpenClipboard(nullptr)) {
|
||||
EmptyClipboard();
|
||||
HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, text.length() + 1);
|
||||
if (hg != nullptr) {
|
||||
memcpy(GlobalLock(hg), text.c_str(), text.length() + 1);
|
||||
GlobalUnlock(hg);
|
||||
SetClipboardData(CF_TEXT, hg);
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
Object init(Env env, Object exports) {
|
||||
exports.Set("getDeviceID", Function::New(env, getDeviceID));
|
||||
exports.Set("getDeviceInfo", Function::New(env, getDeviceInfo));
|
||||
exports.Set("whoUseThePort", Function::New(env, whoUseThePort));
|
||||
exports.Set("copyToClipboard", Function::New(env, copyToClipboard));
|
||||
exports.Set("enablePrivilege", Function::New(env, enablePrivilege));
|
||||
exports.Set("checkGameIsRunning", Function::New(env, checkGameIsRunning));
|
||||
exports.Set("selectGameExecutable", Function::New(env, selectGameExecutable));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(addon, init)
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef REGISTRY_HPP
|
||||
#define REGISTRY_HPP
|
||||
|
||||
#include "../define.h"
|
||||
|
||||
namespace RegUtils {
|
||||
|
||||
DWORD GetInt(Env env, HKEY hKey, const wstring &path, const wstring &value) {
|
||||
DWORD data = 0;
|
||||
DWORD size = sizeof(DWORD);
|
||||
LSTATUS retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_DWORD, nullptr, &data, &size);
|
||||
if (retcode != ERROR_SUCCESS) {
|
||||
Error::New(env, "RegGetValue error: " + to_string(retcode)).ThrowAsJavaScriptException();
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
LSTATUS GetString(HKEY hKey, const wstring &path, const wstring &value, wstring &result) {
|
||||
DWORD size = 0;
|
||||
LSTATUS retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_SZ, nullptr, nullptr, &size);
|
||||
if (retcode != ERROR_SUCCESS) {
|
||||
return retcode;
|
||||
}
|
||||
wstring data;
|
||||
DWORD len = size / sizeof(WCHAR);
|
||||
data.resize(len);
|
||||
retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_SZ, nullptr, &data[0], &size);
|
||||
if (retcode != ERROR_SUCCESS) {
|
||||
return retcode;
|
||||
}
|
||||
data.resize((len-1));
|
||||
result = data;
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
#endif //REGISTRY_HPP
|
||||
@@ -1,80 +0,0 @@
|
||||
#include "utils.h"
|
||||
#include "define.h"
|
||||
|
||||
wstring StringToWString(const string &src) {
|
||||
wstring result;
|
||||
int len = MultiByteToWideChar(CP_ACP, 0, src.c_str(), src.size(), nullptr, 0); // NOLINT(cppcoreguidelines-narrowing-conversions)
|
||||
auto *buffer = new WCHAR[len + 1];
|
||||
MultiByteToWideChar(CP_ACP, 0, src.c_str(), src.size(), buffer, len); // NOLINT(cppcoreguidelines-narrowing-conversions)
|
||||
buffer[len] = '\0';
|
||||
result.append(buffer);
|
||||
delete[] buffer;
|
||||
return result;
|
||||
}
|
||||
|
||||
string WStringToString(const wstring &src) {
|
||||
string result;
|
||||
int len = WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), nullptr, 0, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
|
||||
char *buffer = new char[len + 1];
|
||||
WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), buffer, len, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
|
||||
buffer[len] = '\0';
|
||||
result.append(buffer);
|
||||
delete[] buffer;
|
||||
return result;
|
||||
}
|
||||
|
||||
void Log(Env env, const string &msg) {
|
||||
auto logFunc = env.Global().Get("console").As<Object>().Get("log").As<Function>();
|
||||
logFunc.Call({ Napi::String::New(env, msg) });
|
||||
}
|
||||
|
||||
void Log(Env env, const wstring &msg) {
|
||||
auto logFunc = env.Global().Get("console").As<Object>().Get("log").As<Function>();
|
||||
logFunc.Call({ Napi::String::New(env, WStringToString(msg)) });
|
||||
}
|
||||
|
||||
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent) {
|
||||
OPENFILENAME open;
|
||||
ZeroMemory(&open, sizeof(open));
|
||||
WCHAR file[32768];
|
||||
file[0]=L'\0';
|
||||
open.Flags = OFN_FILEMUSTEXIST | OFN_NONETWORKBUTTON | OFN_PATHMUSTEXIST | OFN_EXPLORER;
|
||||
open.hwndOwner = parent;
|
||||
open.nMaxFile = 32768;
|
||||
open.lpstrFile = file;
|
||||
open.lpstrTitle = L"选择原神主程序";
|
||||
open.lpstrFilter = L"国服/国际服主程序 (YuanShen/GenshinImpact.exe)\0YuanShen.exe;GenshinImpact.exe\0";
|
||||
open.lStructSize = sizeof(open);
|
||||
if(GetOpenFileName(&open)) {
|
||||
result = Napi::String::New(env, WStringToString(file));
|
||||
return ERROR_SUCCESS;
|
||||
} else {
|
||||
return ERROR_ERRORS_ENCOUNTERED;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL EnablePrivilege(Env env, const wstring &name) {
|
||||
HANDLE hToken;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) {
|
||||
Error::New(env, "OpenProcessToken error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
|
||||
return FALSE;
|
||||
}
|
||||
TOKEN_PRIVILEGES tp;
|
||||
ZeroMemory(&tp, sizeof(tp));
|
||||
tp.PrivilegeCount = 1;
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
if (!LookupPrivilegeValue(nullptr, name.c_str(), &tp.Privileges[0].Luid)) {
|
||||
Error::New(env, "LookupPrivilegeValue error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
|
||||
return FALSE;
|
||||
}
|
||||
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr)) {
|
||||
Error::New(env, "AdjustTokenPrivileges error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
|
||||
return FALSE;
|
||||
}
|
||||
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
|
||||
Error::New(env, "The token does not have the specified privilege.").ThrowAsJavaScriptException();
|
||||
return FALSE;
|
||||
}
|
||||
CloseHandle(hToken);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "define.h"
|
||||
|
||||
string WStringToString(const wstring &src);
|
||||
wstring StringToWString(const string &src);
|
||||
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent = GetConsoleWindow());
|
||||
BOOL EnablePrivilege(Env env, const wstring &name);
|
||||
void Log(Env env, const string &msg);
|
||||
void Log(Env env, const wstring &msg);
|
||||
|
||||
#ifndef GENSHIN_EXPORT_NATIVE_UTILS_H
|
||||
#define GENSHIN_EXPORT_NATIVE_UTILS_H
|
||||
|
||||
#endif //GENSHIN_EXPORT_NATIVE_UTILS_H
|
||||
Submodule native/src/wmi deleted from aab36ea1e1
20
package.json
20
package.json
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "genshin-export",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"pkg": "pkg -t node16-win-x64 -C Brotli app.js --build -o app.exe",
|
||||
"pkg-for-windows7": "pkg -t node14-win-x64 -C Brotli app.js --build -o app-win7.exe"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"ini": "^2.0.0",
|
||||
"axios": "^0.26.1",
|
||||
"udp-proxy": "^1.2.0",
|
||||
"protobufjs": "^6.11.2"
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message AllAchievement {
|
||||
repeated Achievement list = 1;
|
||||
}
|
||||
|
||||
message Achievement {
|
||||
|
||||
enum Status {
|
||||
INVALID = 0;
|
||||
UNFINISHED = 1;
|
||||
FINISHED = 2;
|
||||
REWARD_TAKEN = 3;
|
||||
}
|
||||
|
||||
uint32 id = 1;
|
||||
Status status = 2;
|
||||
uint32 current = 3;
|
||||
uint32 require = 4;
|
||||
uint32 finish_timestamp = 5;
|
||||
|
||||
}
|
||||
|
||||
message QueryCurRegion {
|
||||
int32 retcode = 1;
|
||||
string message = 2;
|
||||
msg0 info = 3;
|
||||
oneof group {
|
||||
msg3 field2 = 4;
|
||||
msg4 field3 = 5;
|
||||
}
|
||||
bytes field4 = 11;
|
||||
bytes field5 = 12;
|
||||
bytes field6 = 13;
|
||||
}
|
||||
|
||||
message QueryRegionList {
|
||||
int32 retcode = 1;
|
||||
bytes field0 = 5;
|
||||
bytes field1 = 6;
|
||||
bool field2 = 7;
|
||||
repeated msg2 list = 2;
|
||||
}
|
||||
|
||||
message msg0 {
|
||||
string ip = 1;
|
||||
uint32 port = 2;
|
||||
string field0 = 3;
|
||||
string field1 = 7;
|
||||
string field2 = 8;
|
||||
string field3 = 9;
|
||||
string field4 = 10;
|
||||
string field5 = 11;
|
||||
string field6 = 12;
|
||||
string field7 = 13;
|
||||
uint32 field8 = 14;
|
||||
string field9 = 16;
|
||||
uint32 fieldA = 18;
|
||||
string fieldB = 19;
|
||||
string fieldC = 20;
|
||||
msg1 fieldD = 22;
|
||||
bytes fieldE = 23;
|
||||
string fieldF = 24;
|
||||
string fieldG = 26;
|
||||
string fieldH = 27;
|
||||
bool fieldI = 28;
|
||||
string fieldJ = 29;
|
||||
string fieldK = 30;
|
||||
string fieldL = 31;
|
||||
string fieldM = 32;
|
||||
string fieldN = 33;
|
||||
string fieldO = 34;
|
||||
msg1 fieldP = 35;
|
||||
}
|
||||
|
||||
message msg1 {
|
||||
uint32 field0 = 1;
|
||||
bool field1 = 2;
|
||||
string field2 = 3;
|
||||
string field3 = 4;
|
||||
string field4 = 5;
|
||||
string field5 = 6;
|
||||
string field6 = 7;
|
||||
}
|
||||
|
||||
message msg2 {
|
||||
string field0 = 1;
|
||||
string field1 = 2;
|
||||
string field2 = 3;
|
||||
string url = 4;
|
||||
}
|
||||
|
||||
message msg3 {
|
||||
string field0 = 1;
|
||||
}
|
||||
|
||||
message msg4 {
|
||||
uint32 field0 = 1;
|
||||
uint32 field1 = 2;
|
||||
string field2 = 3;
|
||||
string field3 = 4;
|
||||
}
|
||||
111
regionServer.js
111
regionServer.js
@@ -1,111 +0,0 @@
|
||||
const fs = require("fs")
|
||||
const https = require("https")
|
||||
const axios = require("axios")
|
||||
const { decodeProto, encodeProto, debug } = require("./utils")
|
||||
const path = require("path")
|
||||
const cert = path.join(__dirname, "./cert/root.p12")
|
||||
|
||||
const preparedRegions = {}
|
||||
let currentProxy = undefined
|
||||
|
||||
const getModifiedRegionList = async (conf) => {
|
||||
const d = await axios.get(`https://${conf.dispatchUrl}/query_region_list`, {
|
||||
responseType: "text",
|
||||
params: {
|
||||
version: conf.version,
|
||||
channel_id: conf.channel,
|
||||
sub_channel_id: conf.subChannel
|
||||
}
|
||||
}).catch(_ => {
|
||||
console.log("网络错误,请检查网络后重试 (22-1)")
|
||||
process.exit(221)
|
||||
})
|
||||
const regions = await decodeProto(Buffer.from(d.data,"base64"),"QueryRegionList")
|
||||
if (regions["retcode"] !== 0) {
|
||||
console.log(`系统错误,请稍后重试 (${regions["retcode"]}-23)`)
|
||||
process.exit(23)
|
||||
}
|
||||
regions.list = regions.list.map(item => {
|
||||
const host = new URL(item.url).host
|
||||
if (regions.list.length === 1) {
|
||||
preparedRegions[host] = true
|
||||
}
|
||||
item.url = `https://localdispatch.yuanshen.com/query_cur_region/${host}`
|
||||
return item
|
||||
})
|
||||
return (await encodeProto(regions,"QueryRegionList")).toString("base64")
|
||||
}
|
||||
|
||||
const getModifiedRegionInfo = async (url, uc, hs) => {
|
||||
const splitUrl = url.split("?")
|
||||
const host = splitUrl[0].split("/")[2]
|
||||
const noQueryRequest = splitUrl[1] === undefined
|
||||
const query = noQueryRequest ? "" : `?${splitUrl[1]}`
|
||||
const d = await axios.get(`https://${host}/query_cur_region${query}`, {
|
||||
responseType: "text"
|
||||
}).catch(_ => {
|
||||
console.log("网络错误,请检查网络后重试 (22-2)")
|
||||
process.exit(222)
|
||||
})
|
||||
if (noQueryRequest) {
|
||||
preparedRegions[host] = true
|
||||
return d.data
|
||||
} else {
|
||||
const region = await decodeProto(Buffer.from(d.data,"base64"),"QueryCurRegion")
|
||||
if (region["retcode"] !== 0) {
|
||||
console.log(`${region["message"]} (${region["retcode"]}-24)`)
|
||||
process.exit(24)
|
||||
}
|
||||
const info = region.info
|
||||
if (preparedRegions[host]) {
|
||||
if (currentProxy !== undefined) {
|
||||
currentProxy.close()
|
||||
}
|
||||
debug("Create udp proxy: %s:%d", info.ip, info.port)
|
||||
currentProxy = uc(info.ip, info.port, hs)
|
||||
} else {
|
||||
preparedRegions[host] = true
|
||||
}
|
||||
info.ip = "127.0.0.1"
|
||||
info.port = 45678
|
||||
return (await encodeProto(region,"QueryCurRegion")).toString("base64")
|
||||
}
|
||||
}
|
||||
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
|
||||
const create = async (conf, regionListLoadedCallback, regionSelectCallback) => {
|
||||
const regions = await getModifiedRegionList(conf)
|
||||
const hServer = https.createServer({
|
||||
pfx: fs.readFileSync(cert),
|
||||
passphrase: ""
|
||||
}, async (request, response) => {
|
||||
const url = request.url
|
||||
debug("HTTP: %s", url)
|
||||
response.writeHead(200, { "Content-Type": "text/html" })
|
||||
if (url.startsWith("/query_region_list")) {
|
||||
response.end(regions)
|
||||
} else if (url.startsWith("/query_cur_region")) {
|
||||
const regionInfo = await getModifiedRegionInfo(url, regionSelectCallback, hServer)
|
||||
response.end(regionInfo)
|
||||
} else {
|
||||
const frontResponse = await axios.get(`https://${conf.dispatchIP}${url}`, {
|
||||
responseType: "arraybuffer",
|
||||
httpsAgent: agent
|
||||
}).catch(err => {
|
||||
console.log("网络错误,请检查网络后重试 (22-3)")
|
||||
console.log(err.message)
|
||||
process.exit(223)
|
||||
})
|
||||
response.end(frontResponse.data)
|
||||
}
|
||||
}).listen(443, "127.0.0.1", () => {
|
||||
regionListLoadedCallback()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
310
utils.js
310
utils.js
@@ -1,310 +0,0 @@
|
||||
const fs = require("fs")
|
||||
const dns = require("dns")
|
||||
const ini = require("ini")
|
||||
const zlib = require("zlib")
|
||||
const cloud = require("./secret")
|
||||
const native = require("./native")
|
||||
const readline = require("readline")
|
||||
const protobuf = require("protobufjs")
|
||||
const { version } = require("./version")
|
||||
const { promisify } = require("util")
|
||||
const { createHash } = require("crypto")
|
||||
const path = require("path")
|
||||
const messages = path.join(__dirname, "./proto/Messages.proto")
|
||||
let axios = require("axios")
|
||||
|
||||
const encodeProto = (object, name) => protobuf.load(messages).then(r => {
|
||||
const msgType = r.lookupType(name)
|
||||
const msgInst = msgType.create(object)
|
||||
return msgType.encode(msgInst).finish()
|
||||
})
|
||||
|
||||
const decodeProto = (buf, name) => protobuf.load(messages).then(r => {
|
||||
return r.lookupType(name).decode(buf)
|
||||
})
|
||||
|
||||
const checkPath = path => new Promise((resolve, reject) => {
|
||||
if (!fs.existsSync(`${path}/UnityPlayer.dll`) || !fs.existsSync(`${path}/pkg_version`)) {
|
||||
reject(`路径有误 ${path}`)
|
||||
} else {
|
||||
resolve(path)
|
||||
}
|
||||
})
|
||||
|
||||
let conf
|
||||
|
||||
const initConfig = async () => {
|
||||
const configFileName = "./config.json"
|
||||
if (fs.existsSync(configFileName)) {
|
||||
conf = JSON.parse(fs.readFileSync(configFileName, "utf-8"))
|
||||
} else {
|
||||
conf = {
|
||||
path: [],
|
||||
offlineResource: false,
|
||||
customCDN: ""
|
||||
}
|
||||
const p = path.dirname(native.selectGameExecutable())
|
||||
await checkPath(p).catch(reason => {
|
||||
console.log(reason)
|
||||
process.exit(1)
|
||||
})
|
||||
conf.path.push(p)
|
||||
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
|
||||
}
|
||||
if (conf.proxy !== undefined) {
|
||||
axios = axios.create({
|
||||
proxy: conf.proxy
|
||||
})
|
||||
}
|
||||
if (conf.path.length === 1) {
|
||||
await checkPath(conf.path[0]).catch(reason => {
|
||||
console.log(reason)
|
||||
process.exit(1)
|
||||
}).then(p => {
|
||||
conf.path = p
|
||||
})
|
||||
} else {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
const question = (query) => new Promise(resolve => {
|
||||
rl.question(query, resolve)
|
||||
})
|
||||
const idx = await question(`选择客户端: \n${conf.path.map((s, i) => {
|
||||
const fp = fs.existsSync(`${s}/GenshinImpact.exe`) ? `${s}\\GenshinImpact.exe` : `${s}\\YuanShen.exe`
|
||||
return `[${i}] ${fp}`
|
||||
}).join("\n")}\n> `)
|
||||
await checkPath(conf.path[parseInt(idx)]).catch(reason => {
|
||||
console.log(reason)
|
||||
process.exit(1)
|
||||
}).then(p => {
|
||||
conf.path = p
|
||||
})
|
||||
rl.close()
|
||||
}
|
||||
conf.isOversea = fs.existsSync(conf.path + "/GenshinImpact.exe")
|
||||
conf.dataDir = conf.isOversea ? conf.path + "/GenshinImpact_Data" : conf.path + "/YuanShen_Data"
|
||||
const readGameRes = (path) => fs.readFileSync(conf.dataDir + path, "utf-8")
|
||||
const genshinConf = ini.parse(fs.readFileSync(`${conf.path}/config.ini`, "utf-8"))["General"]
|
||||
conf.channel = genshinConf["channel"]
|
||||
conf.subChannel = genshinConf["sub_channel"]
|
||||
conf.version = readGameRes("/Persistent/ChannelName") + readGameRes("/Persistent/ScriptVersion")
|
||||
conf.executable = conf.isOversea ? `${conf.path}/GenshinImpact.exe` : `${conf.path}/YuanShen.exe`
|
||||
conf.dispatchUrl = `dispatch${conf.isOversea ? "os" : "cn"}global.yuanshen.com`
|
||||
conf.dispatchIP = (await promisify(dns.lookup).bind(dns)(conf.dispatchUrl, 4)).address
|
||||
return conf
|
||||
}
|
||||
|
||||
const checkGameIsRunning = () => {
|
||||
const name = path.basename(conf.executable)
|
||||
if (native.checkGameIsRunning(name)) {
|
||||
console.log("原神正在运行,请关闭后重试")
|
||||
process.exit(301)
|
||||
}
|
||||
}
|
||||
|
||||
const checkPortIsUsing = () => {
|
||||
const portUsage = native.whoUseThePort(443)
|
||||
if (portUsage !== undefined) {
|
||||
console.log(`443端口被 ${path.basename(portUsage["path"])}(${portUsage["pid"]}) 占用,结束该进程后重试`)
|
||||
process.exit(14)
|
||||
}
|
||||
}
|
||||
|
||||
const splitPacket = buf => {
|
||||
let offset = 0
|
||||
let arr = []
|
||||
while (offset < buf.length) {
|
||||
let dataLength = buf.readUInt32LE(offset + 24)
|
||||
arr.push(buf.subarray(offset, offset + 28 + dataLength))
|
||||
offset += dataLength + 28
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
const md5 = str => {
|
||||
const h = createHash("md5")
|
||||
h.update(str)
|
||||
return h.digest("hex")
|
||||
}
|
||||
|
||||
let cdnUrlFormat = null
|
||||
|
||||
String.prototype.format = function() {
|
||||
const args = arguments;
|
||||
return this.replace(/{(\d+)}/g, (match, number) => typeof args[number] != "undefined" ? args[number] : match)
|
||||
}
|
||||
|
||||
const checkCDN = async () => {
|
||||
try {
|
||||
cdnUrlFormat = "https://cdn.jsdelivr.net/gh/{0}@master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
try {
|
||||
cdnUrlFormat = "https://raw.githubusercontent.com/{0}/master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
try {
|
||||
const s = conf === undefined ? "" : conf.customCDN.trim()
|
||||
if (s.length > 0) {
|
||||
cdnUrlFormat = s
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
cdnUrlFormat = "https://raw.fastgit.org/{0}/master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
try {
|
||||
cdnUrlFormat = "https://ghproxy.net/https://raw.githubusercontent.com/{0}/master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
throw "网络错误,请检查配置文件/网络后重试 (11-3)"
|
||||
}
|
||||
|
||||
const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
|
||||
log(`预检资源: ${cdnUrlFormat.format(repo, fp)}`)
|
||||
fs.mkdirSync("./cache", { recursive: true })
|
||||
const localPath = `./cache/${md5(fp)}`
|
||||
if (conf.offlineResource) {
|
||||
const fd = brotliDecompressSync(fs.readFileSync(localPath))
|
||||
return JSON.parse(fd.subarray(1 + fd.readUInt8()).toString())
|
||||
}
|
||||
const header = {}
|
||||
let fd = Buffer.alloc(0)
|
||||
if (fs.existsSync(localPath)) {
|
||||
fd = brotliDecompressSync(fs.readFileSync(localPath))
|
||||
const etagLength = fd.readUInt8()
|
||||
header["If-None-Match"] = fd.subarray(1, 1 + etagLength).toString()
|
||||
}
|
||||
const headResponse = await axios.head(cdnUrlFormat.format(repo, fp), {
|
||||
headers: header,
|
||||
validateStatus: _ => true
|
||||
})
|
||||
if (headResponse.status === 304) {
|
||||
log("%s 命中缓存", fp)
|
||||
const etagLength = fd.readUInt8()
|
||||
return JSON.parse(fd.subarray(1 + etagLength).toString())
|
||||
} else {
|
||||
log("下载所需资源, 请稍后...")
|
||||
const response = await axios.get(cdnUrlFormat.format(repo, fp))
|
||||
const etag = response.headers.etag
|
||||
const str = JSON.stringify(response.data)
|
||||
const comp = brotliCompressSync(Buffer.concat([Buffer.of(etag.length), Buffer.from(etag), Buffer.from(str)]))
|
||||
fs.writeFileSync(localPath, comp)
|
||||
log("下载完成")
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
const isDebug = false
|
||||
|
||||
const debug = (msg, ...params) => {
|
||||
if (isDebug) log(msg, ...params)
|
||||
}
|
||||
|
||||
const log = (msg, ...params) => {
|
||||
const time = new Date()
|
||||
const timeStr = time.getHours().toString().padStart(2, "0") + ":" + time.getMinutes().toString().padStart(2, "0") + ":" + time.getSeconds().toString().padStart(2, "0")
|
||||
console.log(`${timeStr} ${msg}`, ...params)
|
||||
}
|
||||
|
||||
const upload = async data => {
|
||||
return await cloud.post("/achievement-export", data)
|
||||
}
|
||||
|
||||
const checkUpdate = async () => {
|
||||
const data = (await cloud.get("/latest-version")).data
|
||||
if (data["vc"] !== version.code) {
|
||||
log(`有可用更新: ${version.name} => ${data["vn"]}`)
|
||||
log(`更新内容: \n${data["ds"]}`)
|
||||
log("下载地址: https://github.com/HolographicHat/genshin-achievement-export/releases\n")
|
||||
}
|
||||
}
|
||||
|
||||
const brotliCompressSync = data => zlib.brotliCompressSync(data,{
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
|
||||
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: data.length
|
||||
}
|
||||
})
|
||||
|
||||
const brotliDecompressSync = data => zlib.brotliDecompressSync(data)
|
||||
|
||||
let hostsContent = undefined
|
||||
|
||||
const setupHost = (restore = false) => {
|
||||
if (restore && hostsContent === undefined) return
|
||||
const path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.writeFileSync(path, "")
|
||||
}
|
||||
fs.chmodSync(path, 0o777)
|
||||
if (restore) {
|
||||
fs.writeFileSync(path, hostsContent)
|
||||
} else {
|
||||
hostsContent = fs.readFileSync(path, "utf-8")
|
||||
const requireHosts = new Map()
|
||||
requireHosts.set(conf.dispatchUrl, "127.0.0.1")
|
||||
requireHosts.set("localdispatch.yuanshen.com", "127.0.0.1")
|
||||
const currentHosts = new Map()
|
||||
hostsContent.split("\n").map(l => l.trim()).filter(l => !l.startsWith("#") && l.length > 0).forEach(value => {
|
||||
const pair = value.trim().split(" ").filter(v => v.trim().length !== 0)
|
||||
currentHosts.set(pair[1], pair[0])
|
||||
})
|
||||
requireHosts.forEach((value, key) => {
|
||||
if (currentHosts.has(key)) {
|
||||
if (currentHosts.get(key) === value) {
|
||||
requireHosts.delete(key)
|
||||
} else {
|
||||
currentHosts.delete(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
requireHosts.forEach((ip, host) => {
|
||||
currentHosts.set(host, ip)
|
||||
})
|
||||
const newContent = Array.from(currentHosts.entries()).map(pair => {
|
||||
return `${pair[1]} ${pair[0]}`
|
||||
}).join("\n")
|
||||
fs.writeFileSync(path, newContent)
|
||||
}
|
||||
debug("修改SystemHosts")
|
||||
process.on("exit", () => {
|
||||
fs.writeFileSync(path, hostsContent)
|
||||
})
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
class KPacket {
|
||||
|
||||
constructor(data) {
|
||||
this.origin = data
|
||||
this.conv = data.readUInt32BE(0)
|
||||
this.token = data.readUInt32BE(4)
|
||||
this.cmd = data.readUInt8(8)
|
||||
this.frg = data.readUInt8(9)
|
||||
this.wnd = data.readUInt16LE(10)
|
||||
this.ts = data.readUInt32LE(12)
|
||||
this.sn = data.readUInt32LE(16)
|
||||
this.una = data.readUInt32LE(20)
|
||||
this.length = data.readUInt32LE(24)
|
||||
this.data = data.subarray(28)
|
||||
this.hash = (() => {
|
||||
const h = createHash("sha256")
|
||||
h.update(Buffer.concat([Buffer.of(this.sn, this.frg), this.data]))
|
||||
return h.digest("hex")
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
log, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
|
||||
setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat, checkGameIsRunning, checkPortIsUsing
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
const version = {
|
||||
code: 5,
|
||||
name: "1.4"
|
||||
}
|
||||
|
||||
module.exports = { version }
|
||||
Reference in New Issue
Block a user