mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-08 07:32:47 +08:00
Compare commits
318 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
8a0c82f89f | ||
|
|
d64bf8149e | ||
|
|
82e85f5ea0 | ||
|
|
891a19c3f7 | ||
|
|
7b94964342 | ||
|
|
d5e290e866 | ||
|
|
f7c48472f1 | ||
|
|
a6b80d3588 | ||
|
|
18e3d3ffe3 | ||
|
|
a22ad8e87d | ||
|
|
35ec8b5859 | ||
|
|
35899e74f6 | ||
|
|
bb4d5215f1 | ||
|
|
e4e76286c9 | ||
|
|
be5a457639 | ||
|
|
945a94222a | ||
|
|
3d03496074 | ||
|
|
1e6ebc76dd | ||
|
|
ab47ff2b06 | ||
|
|
5fe6e80cc8 | ||
|
|
a23d7f3cc9 | ||
|
|
0ab6444b6f | ||
|
|
00f0aaa4a6 | ||
|
|
2377dbd492 | ||
|
|
0b015886ea | ||
|
|
4adfc9a312 | ||
|
|
b4cd69f303 | ||
|
|
c76ddd9e3f | ||
|
|
d40c456494 | ||
|
|
408169da4e | ||
|
|
9de8e957fd | ||
|
|
a287e5db43 | ||
|
|
589048b912 | ||
|
|
79517a37d2 | ||
|
|
e022f04661 | ||
|
|
d1a635be7c | ||
|
|
aa853262b7 | ||
|
|
bdf9561dfa | ||
|
|
a663fddeb0 |
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
|
||||
30
.github/workflows/dotnet.yml
vendored
Normal file
30
.github/workflows/dotnet.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: .NET Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- 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
|
||||
run: dotnet publish --property:OutputPath=.\publish\
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Artifacts
|
||||
path: publish
|
||||
28
.github/workflows/lib-nuget.yml
vendored
Normal file
28
.github/workflows/lib-nuget.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: YaeLib NuGet Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Restore NuGet Packages
|
||||
run: nuget restore lib\YaeAchievementLib.sln
|
||||
|
||||
- name: Build
|
||||
continue-on-error: true
|
||||
run: msbuild lib\YaeAchievementLib.sln /p:Configuration=Release
|
||||
|
||||
- name: Pack
|
||||
run: nuget pack lib\YaeAchievementLib.nuspec
|
||||
|
||||
- name: Publish to NuGet
|
||||
run: nuget push *.nupkg ${{ secrets.NUGET_API_KEY }} -src https://api.nuget.org/v3/index.json
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,7 +1,15 @@
|
||||
cache
|
||||
config.json
|
||||
out.*
|
||||
node_modules
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea
|
||||
secret.js
|
||||
package-lock.json
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.vs/
|
||||
|
||||
src/Proto/*
|
||||
|
||||
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
|
||||
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/>
|
||||
|
||||
82
README.md
82
README.md
@@ -1,40 +1,42 @@
|
||||
# 原神成就导出工具
|
||||
|
||||
   
|
||||
|
||||
- 支持导出所有成就
|
||||
- 没有窗口大小游戏或语言等要求
|
||||
- 更快、更准
|
||||
|
||||
## 使用说明
|
||||
**打开程序前需要关闭正在运行的原神主程序**
|
||||
第一次打开需要先设置原神主程序所在路径,支持多个路径, 使用符号'*'分隔
|
||||

|
||||
- 自定义代理: 配置文件内添加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)
|
||||
|
||||
## FAQ
|
||||
1. Q: 为什么需要管理员权限
|
||||
A: 临时修改Hosts和启动原神
|
||||
2. Q: 报毒?
|
||||
A: 执行命令或修改hosts引发,相关代码可在./utils.js,./appcenter.js下找到
|
||||
|
||||
## TODO
|
||||
- [ ] GUI
|
||||
<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>
|
||||
|
||||
- 支持导出所有类别的成就
|
||||
- 支持官服,渠道服与国际服
|
||||
- 没有窗口大小、游戏语言等要求
|
||||
|
||||
## 导出支持
|
||||
|
||||
> 按照数字键选择导出方式,<kbd>0</kbd> 为默认导出方式
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. 表格文件 `.csv`
|
||||
5. [寻空](https://github.com/xunkong/xunkong)
|
||||
6. [原魔工具箱](https://apps.apple.com/app/id1663989619)
|
||||
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON 文件
|
||||
|
||||
## 使用说明
|
||||
→ [Tutorial.md](Tutorial.md)
|
||||
|
||||
## 下载地址
|
||||
[releases/latest](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## 问题反馈
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
|
||||
## 常见问题
|
||||
0. Q: 打不开
|
||||
A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer)
|
||||
|
||||
1. Q: 原神启动时报错: 数据异常(31-4302)
|
||||
A: 不要把软件和原神主程序放一起
|
||||
|
||||
44
README_EN.md
Normal file
44
README_EN.md
Normal file
@@ -0,0 +1,44 @@
|
||||
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
|
||||
|
||||
# YaeAchievement
|
||||
|
||||
    
|
||||
|
||||
[简体中文](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
|
||||
|
||||
> Select the export method according to the number keys, <kbd>0</kbd> is the default export method
|
||||
|
||||
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群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
|
||||
## Frequently asked questions
|
||||
0. Q: Unable to start
|
||||
A: Download and install [.NET Runtime 7](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer) or ` winget install Microsoft.DotNet.Runtime.8`
|
||||
|
||||
|
||||
1. Q: Error while Genshin started: Data Exception (31-4302)
|
||||
A: Don't place software in the directory containing Genshin Impact.
|
||||
|
||||
44
README_JP.md
Normal file
44
README_JP.md
Normal file
@@ -0,0 +1,44 @@
|
||||
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
|
||||
|
||||
# YaeAchievement
|
||||
|
||||
    
|
||||
|
||||
[简体中文](README.md) | [English](README_EN.md) | 日本語
|
||||
|
||||
</div>
|
||||
|
||||
- すべてのカテゴリの実績のエクスポートをサポート
|
||||
- すべてのバージョンの原神をサポート
|
||||
- ウィンドウサイズ、ゲーム言語などの要件はありません
|
||||
|
||||
## エクスポートサポート
|
||||
|
||||
> 数字キーに従ってエクスポート方法を選択します。<kbd>0</kbd> はデフォルトのエクスポート方法です
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
3. [Seelie.me](https://seelie.me/achievements)
|
||||
4. フォームファイル `.csv`
|
||||
5. [尋空](https://github.com/xunkong/xunkong)
|
||||
6. [原魔ツールボックス](https://apps.apple.com/app/id1663989619)
|
||||
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
|
||||
8. [UIAF](https://uigf.org/standards/UIAF.html) JSONファイル
|
||||
|
||||
## 使用説明書:
|
||||
→ [Tutorial_JP.md](Tutorial_JP.md)
|
||||
|
||||
## ダウンロード: [こちら](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## フィードバックや問題?
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues) または [QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
|
||||
## よくある質問
|
||||
0. Q: 起動できない
|
||||
A: [.NET Runtime 7](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer) をダウンロードしてインストールするか、`winget install Microsoft.DotNet.Runtime.8` を実行してください。
|
||||
|
||||
|
||||
1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302)
|
||||
A: ソフトウェアを原神のディレクトリに配置しないでください。
|
||||
|
||||
81
Tutorial.md
Normal file
81
Tutorial.md
Normal file
@@ -0,0 +1,81 @@
|
||||
## 使用说明
|
||||
|
||||
1.选择正确的下载文件(以2.4.1版本为例):
|
||||
|
||||
点击该网址:https://github.com/HolographicHat/YaeAchievement/releases
|
||||
|
||||
点击图中红框圈中的名称为“YaeAchievement.exe”的文件,浏览器会自动弹出下载。建议将该文件保存在桌面或者其它易于寻找的文件夹内。
|
||||
|
||||

|
||||
|
||||
2.安装启动软件所需文件(若已安装该运行时可忽略此步骤)
|
||||
|
||||
点击该网址:https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer 。
|
||||
|
||||
进入网页后浏览器会自动弹出下载,同样地,将文件保存在桌面或者其它易于寻找的文件夹内。
|
||||
|
||||
下载完成后打开名称形如dotnet-runtime-x.x.x-win-x64.exe的文件,会弹出安装窗口,如下图所示。
|
||||
|
||||

|
||||
|
||||
直接点击安装即可。
|
||||
|
||||
3.打开主程序所需的操作以及成就导出的选择
|
||||
|
||||
双击在第一步下载的名称为“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 文件
|
||||
76
Tutorial_EN.md
Normal file
76
Tutorial_EN.md
Normal file
@@ -0,0 +1,76 @@
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
2.Install .NET Runtime 7 (this step can be ignored if the runtime is already installed)
|
||||
|
||||
Click Here:https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer .
|
||||
|
||||
Or `winget install Microsoft.DotNet.Runtime.7` if you use Windows 11 or have Winget installed.
|
||||
|
||||
The browser automatically pops up and downloads when you enter the web page, as well as saving files in a desktop or other easy-to-see folder.
|
||||
|
||||
When you open a file with the name dotnet-runtime-x.x.x-win-x64.exe after the download is complete, an installation window pops up, as shown below.
|
||||
|
||||

|
||||
|
||||
Just click Install.
|
||||
|
||||
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
|
||||
75
Tutorial_JP.md
Normal file
75
Tutorial_JP.md
Normal file
@@ -0,0 +1,75 @@
|
||||
## 使用説明書
|
||||
|
||||
1. YaeAchievement(最新バージョン)をダウンロード:
|
||||
|
||||
こちらをクリック:https://github.com/HolographicHat/YaeAchievement/releases
|
||||
|
||||
赤枠で囲まれた「YaeAchievement.exe」という名前のファイルをクリックすると、自動的にポップアップしてダウンロードされます。このファイルをデスクトップや他の見やすいフォルダに保存することをお勧めします。
|
||||
|
||||

|
||||
|
||||
2. .NET Runtime 7をインストール(ランタイムが既にインストールされている場合はこのステップを無視できます)
|
||||
|
||||
こちらをクリック:https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer .
|
||||
|
||||
または、Windows 11を使用しているか、Wingetがインストールされている場合は、`winget install Microsoft.DotNet.Runtime.7`を実行します。
|
||||
|
||||
ウェブページにアクセスすると、ブラウザが自動的にポップアップしてダウンロードされます。ファイルをデスクトップや他の見やすいフォルダに保存します。
|
||||
|
||||
ダウンロードが完了したら、dotnet-runtime-x.x.x-win-x64.exeという名前のファイルを開くと、インストールウィンドウがポップアップします。以下の図のように表示されます。
|
||||
|
||||

|
||||
|
||||
インストールをクリックするだけです。
|
||||
|
||||
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ファイル
|
||||
3
YaeAchievement.slnx
Normal file
3
YaeAchievement.slnx
Normal file
@@ -0,0 +1,3 @@
|
||||
<Solution>
|
||||
<Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#"/>
|
||||
</Solution>
|
||||
62
YaeAchievement/YaeAchievement.csproj
Normal file
62
YaeAchievement/YaeAchievement.csproj
Normal file
@@ -0,0 +1,62 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ApplicationManifest>res\app.manifest</ApplicationManifest>
|
||||
<AssemblyVersion>5.3.0</AssemblyVersion>
|
||||
<FileVersion>5.3.0</FileVersion>
|
||||
<ApplicationIcon>res\icon.ico</ApplicationIcon>
|
||||
|
||||
<DebugType>embedded</DebugType>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.0" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Grpc.Tools" Version="2.67.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Spectre.Console" Version="0.49.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="res\App.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>App.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<None Remove="res\Updater.exe" />
|
||||
<None Remove="src\NativeMethods.json" />
|
||||
<None Remove="src\NativeMethods.txt" />
|
||||
<AdditionalFiles Include="src\NativeMethods.json" />
|
||||
<AdditionalFiles Include="src\NativeMethods.txt" />
|
||||
<EmbeddedResource Include="res\Updater.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="res\App.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>App.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
406
YaeAchievement/res/App.Designer.cs
generated
Normal file
406
YaeAchievement/res/App.Designer.cs
generated
Normal file
@@ -0,0 +1,406 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <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 You need to login genshin impact before exporting..
|
||||
/// </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:
|
||||
///[0] Cocogoat (https://cocogoat.work/achievement, Default)
|
||||
///[1] Snap.HuTao
|
||||
///[2] Paimon.moe
|
||||
///[3] Seelie.me
|
||||
///[4] Csv file
|
||||
///[5] Xunkong
|
||||
///[7] Teyvat Guide
|
||||
///[8] UIAF JSON File
|
||||
///Input a number (0-8): .
|
||||
/// </summary>
|
||||
internal static string ExportChoose {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportChoose", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fail, please contact developer to get help information.
|
||||
/// </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 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 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 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 Has update: {0} => {1}.
|
||||
/// </summary>
|
||||
internal static string UpdateNewVersion {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateNewVersion", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Byte[].
|
||||
/// </summary>
|
||||
internal static byte[] Updater {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("Updater", resourceCulture);
|
||||
return ((byte[])(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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? (yes|no).
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
YaeAchievement/res/App.resx
Normal file
143
YaeAchievement/res/App.resx
Normal file
@@ -0,0 +1,143 @@
|
||||
<?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</value>
|
||||
</data>
|
||||
<data name="AllAchievement" xml:space="preserve">
|
||||
<value>all achievement</value>
|
||||
</data>
|
||||
<data name="ExportChoose" xml:space="preserve">
|
||||
<value>Export to:
|
||||
[0] Cocogoat (https://cocogoat.work/achievement, Default)
|
||||
[1] Snap.HuTao
|
||||
[2] Paimon.moe
|
||||
[3] Seelie.me
|
||||
[4] Csv file
|
||||
[5] Xunkong
|
||||
[7] Teyvat Guide
|
||||
[8] UIAF JSON File
|
||||
Input a number (0-8): </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>You need to login genshin impact before exporting.</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? (yes|no)</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>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="Updater" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>Updater.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
<data name="GenshinHashError" xml:space="preserve">
|
||||
<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>
|
||||
</root>
|
||||
133
YaeAchievement/res/App.zh.resx
Normal file
133
YaeAchievement/res/App.zh.resx
Normal file
@@ -0,0 +1,133 @@
|
||||
<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>导出失败, 请联系开发者以获取帮助</value>
|
||||
</data>
|
||||
<data name="AllAchievement" xml:space="preserve">
|
||||
<value>全部成就</value>
|
||||
</data>
|
||||
<data name="ExportChoose" xml:space="preserve">
|
||||
<value>导出至:
|
||||
[0] 椰羊 (https://cocogoat.work/achievement, 默认)
|
||||
[1] Snap Hutao
|
||||
[2] Paimon.moe
|
||||
[3] Seelie.me
|
||||
[4] 表格文件
|
||||
[5] 寻空
|
||||
[6] 原魔工具箱
|
||||
[7] Teyvat Guide
|
||||
[8] UIAF JSON 文件
|
||||
输入一个数字 (0-8): </value>
|
||||
</data>
|
||||
<data 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>要使用上一次获取到的成就数据吗? (yes|no)</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>
|
||||
</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 |
25
YaeAchievement/res/proto/AchievementInfo.proto
Normal file
25
YaeAchievement/res/proto/AchievementInfo.proto
Normal file
@@ -0,0 +1,25 @@
|
||||
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 AchievementInfo {
|
||||
string version = 1;
|
||||
map<uint32, string> group = 2;
|
||||
map<uint32, AchievementItem> items = 3;
|
||||
AchievementProtoFieldInfo pb_info = 4;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
15
YaeAchievement/res/proto/UpdateInfo.proto
Normal file
15
YaeAchievement/res/proto/UpdateInfo.proto
Normal file
@@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Proto";
|
||||
|
||||
message UpdateInfo {
|
||||
uint32 version_code = 1;
|
||||
string version_name = 2;
|
||||
string description = 3;
|
||||
string package_link = 4;
|
||||
bool force_update = 5;
|
||||
bool enable_lib_download = 6;
|
||||
bool enable_auto_update = 7;
|
||||
string current_cn_hash = 8;
|
||||
string current_os_hash = 9;
|
||||
}
|
||||
60
YaeAchievement/src/AppConfig.cs
Normal file
60
YaeAchievement/src/AppConfig.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using YaeAchievement.res;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static partial class AppConfig {
|
||||
|
||||
public static string GamePath { get; private set; } = null!;
|
||||
|
||||
private static readonly string[] ProductNames = [ "原神", "Genshin Impact" ];
|
||||
|
||||
internal static void Load(string argumentPath) {
|
||||
if (argumentPath != "auto" && File.Exists(argumentPath)) {
|
||||
GamePath = argumentPath;
|
||||
return;
|
||||
}
|
||||
var pathCacheFile = new CacheFile("genshin_impact_game_path");
|
||||
if (pathCacheFile.Exists()) {
|
||||
var path = pathCacheFile.Read().Content.ToStringUtf8();
|
||||
if (path != null && File.Exists(path)) {
|
||||
GamePath = path;
|
||||
return;
|
||||
}
|
||||
}
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var logPath = ProductNames
|
||||
.Select(name => $"{appDataPath}/../LocalLow/miHoYo/{name}/output_log.txt")
|
||||
.Where(File.Exists)
|
||||
.MaxBy(File.GetLastWriteTime);
|
||||
if (logPath == null) {
|
||||
throw new ApplicationException(App.ConfigNeedStartGenshin);
|
||||
}
|
||||
GamePath = GetGamePathFromLogFile(logPath)
|
||||
?? GetGamePathFromLogFile($"{logPath}.last")
|
||||
?? throw new ApplicationException(App.ConfigNeedStartGenshin);
|
||||
}
|
||||
|
||||
private static string? GetGamePathFromLogFile(string path) {
|
||||
if (!File.Exists(path)) {
|
||||
return null;
|
||||
}
|
||||
var copiedLogFilePath = Path.GetTempFileName();
|
||||
File.Copy(path, copiedLogFilePath, true);
|
||||
var content = File.ReadAllText(copiedLogFilePath);
|
||||
try {
|
||||
File.Delete(copiedLogFilePath);
|
||||
} catch (Exception) { /* ignore */ }
|
||||
var matchResult = GamePathRegex().Match(content);
|
||||
if (!matchResult.Success) {
|
||||
return null;
|
||||
}
|
||||
var entryName = matchResult.Groups["1"].Value.Replace("_Data", ".exe");
|
||||
return Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(?m).:/.+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex GamePathRegex();
|
||||
|
||||
}
|
||||
234
YaeAchievement/src/Export.cs
Normal file
234
YaeAchievement/src/Export.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Win32;
|
||||
using YaeAchievement.Outputs;
|
||||
using YaeAchievement.Parsers;
|
||||
using YaeAchievement.res;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class Export {
|
||||
|
||||
public static uint ExportTo { get; set; } = uint.MaxValue;
|
||||
|
||||
public static void Choose(AchievementAllDataNotify data) {
|
||||
if (ExportTo == uint.MaxValue) {
|
||||
Console.Write(App.ExportChoose);
|
||||
while (Console.KeyAvailable) {
|
||||
Console.ReadKey(false);
|
||||
}
|
||||
if (!uint.TryParse(Console.ReadLine(), out var num)) num = 0;
|
||||
ExportTo = num;
|
||||
}
|
||||
((Action<AchievementAllDataNotify>) (ExportTo switch {
|
||||
1 => ToHuTao,
|
||||
2 => ToPaimon,
|
||||
3 => ToSeelie,
|
||||
4 => ToCSV,
|
||||
5 => ToXunkong,
|
||||
6 => ToWxApp1,
|
||||
7 => ToTeyvatGuide,
|
||||
8 => ToUIAFJson,
|
||||
9 => ToRawJson,
|
||||
_ => ToCocogoat
|
||||
})).Invoke(data);
|
||||
}
|
||||
|
||||
private 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) {
|
||||
Console.WriteLine(App.ExportToCocogoatFail);
|
||||
return;
|
||||
}
|
||||
var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
var responseJson = JsonSerializer.Deserialize(responseText, CocogoatResponseContext.Default.CocogoatResponse)!;
|
||||
var cocogoatUrl = $"https://cocogoat.work/achievement?memo={responseJson.Key}";
|
||||
Console.WriteLine(cocogoatUrl);
|
||||
if (Utils.ShellOpen(cocogoatUrl))
|
||||
{
|
||||
Console.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);
|
||||
Console.WriteLine(App.ExportToWxApp1Success, id);
|
||||
}
|
||||
|
||||
private static void ToHuTao(AchievementAllDataNotify data) {
|
||||
if (CheckWinUIAppScheme("hutao")) {
|
||||
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
|
||||
Utils.ShellOpen("hutao://achievement/import");
|
||||
Console.WriteLine(App.ExportToSnapGenshinSuccess);
|
||||
} else {
|
||||
Console.WriteLine(App.ExportToSnapGenshinNeedUpdate);
|
||||
Utils.ShellOpen("ms-windows-store://pdp/?productid=9PH4NXJ2JN52");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToXunkong(AchievementAllDataNotify data) {
|
||||
if (CheckWinUIAppScheme("xunkong")) {
|
||||
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
|
||||
Utils.ShellOpen("xunkong://import-achievement?caller=YaeAchievement&from=clipboard");
|
||||
Console.WriteLine(App.ExportToXunkongSuccess);
|
||||
} else {
|
||||
Console.WriteLine(App.ExportToXunkongNeedUpdate);
|
||||
Utils.ShellOpen("ms-windows-store://pdp/?productid=9N2SVG0JMT12");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToTeyvatGuide(AchievementAllDataNotify data) {
|
||||
if (Process.GetProcessesByName("TeyvatGuide").Length != 0) {
|
||||
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
|
||||
Utils.ShellOpen("teyvatguide://import_uigf?app=YaeAchievement");
|
||||
Console.WriteLine(App.ExportToTauriSuccess);
|
||||
} else {
|
||||
Console.WriteLine(App.ExportToTauriFail);
|
||||
Utils.ShellOpen("ms-windows-store://pdp/?productid=9NLBNNNBNSJN");
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static void ToUIAFJson(AchievementAllDataNotify data) {
|
||||
var path = Path.GetFullPath($"uiaf-{DateTime.Now:yyyyMMddHHmmss}.json");
|
||||
if (TryWriteToFile(path, UIAFSerializer.Serialize(data))) {
|
||||
Console.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))) {
|
||||
Console.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))) {
|
||||
Console.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) {
|
||||
Console.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)}")) {
|
||||
Console.WriteLine(App.ExportToFileSuccess, path);
|
||||
Process.Start("explorer.exe", $"{Path.GetDirectoryName(path)}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToRawJson(AchievementAllDataNotify data) {
|
||||
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-raw.json");
|
||||
var text = AchievementRawDataSerializer.Serialize(data);
|
||||
if (TryWriteToFile(path, text)) {
|
||||
Console.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
|
||||
Console.WriteLine($"{msg}: {ex.Message}");
|
||||
return ex.NativeErrorCode;
|
||||
}
|
||||
|
||||
private static bool TryWriteToFile(string path, string contents) {
|
||||
try {
|
||||
File.WriteAllText(path, contents);
|
||||
return true;
|
||||
} catch (UnauthorizedAccessException) {
|
||||
Console.WriteLine(App.NoWritePermission, path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WxApp1Root {
|
||||
|
||||
public string Key { get; init; } = null!;
|
||||
|
||||
public UIAFRoot Data { get; init; } = null!;
|
||||
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(WxApp1Root))]
|
||||
[JsonSourceGenerationOptions(
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class WxApp1Serializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf, string key) => JsonSerializer.Serialize(new WxApp1Root {
|
||||
Key = key,
|
||||
Data = Outputs.UIAFRoot.FromNotify(ntf)
|
||||
}, Default.WxApp1Root);
|
||||
}
|
||||
|
||||
public record CocogoatResponse(string Key);
|
||||
|
||||
[JsonSerializable(typeof(CocogoatResponse))]
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
public partial class CocogoatResponseContext : JsonSerializerContext;
|
||||
41
YaeAchievement/src/Extensions.cs
Normal file
41
YaeAchievement/src/Extensions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public static class Extensions {
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<MD5> md5 = new (MD5.Create);
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<SHA1> sha1 = new (SHA1.Create);
|
||||
|
||||
public static byte[] ToBytes(this string text) {
|
||||
return Encoding.UTF8.GetBytes(text);
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string MD5Hash(this string text) {
|
||||
return text.ToBytes().MD5Hash();
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string MD5Hash(this byte[] data) {
|
||||
return md5.Value.ComputeHash(data).ToHex().ToLower();
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string SHA1Hash(this string text, bool base64 = true) {
|
||||
var bytes = sha1.Value.ComputeHash(text.ToBytes());
|
||||
return base64 ? bytes.ToBase64() : bytes.ToHex();
|
||||
}
|
||||
|
||||
public static string ToHex(this byte[] bytes) {
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
|
||||
public static string ToBase64(this byte[] bytes) {
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
42
YaeAchievement/src/GlobalVars.cs
Normal file
42
YaeAchievement/src/GlobalVars.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Proto;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable ConvertToConstant.Global
|
||||
// ReSharper disable FieldCanBeMadeReadOnly.Global
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
|
||||
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 = 235;
|
||||
public const string AppVersionName = "5.3";
|
||||
|
||||
public const string PipeName = "YaeAchievementPipe";
|
||||
public const string RinBucketHost = "https://rin.holohat.work";
|
||||
public const string SakuraBucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
|
||||
|
||||
public static CacheFile AchievementDataCache { get; } = new ("achievement_data");
|
||||
|
||||
[field:MaybeNull]
|
||||
public static AchievementInfo AchievementInfo =>
|
||||
field ??= AchievementInfo.Parser.ParseFrom(Utils.GetBucketFile("schicksal/metadata").GetAwaiter().GetResult());
|
||||
|
||||
static GlobalVars() {
|
||||
Directory.CreateDirectory(DataPath);
|
||||
Directory.CreateDirectory(CachePath);
|
||||
}
|
||||
|
||||
}
|
||||
63
YaeAchievement/src/Injector.cs
Normal file
63
YaeAchievement/src/Injector.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.ComponentModel;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Memory;
|
||||
using Windows.Win32.System.Threading;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class Injector {
|
||||
|
||||
public static unsafe bool CreateProcess(string path, out HANDLE hProc, out HANDLE hThread, out uint pid) {
|
||||
Span<char> cmdLines = stackalloc char[1]; // "\0"
|
||||
var si = new STARTUPINFOW {
|
||||
cb = unchecked((uint)sizeof(STARTUPINFOW))
|
||||
};
|
||||
var dir = Path.GetDirectoryName(path)!;
|
||||
var result = Native.CreateProcess(
|
||||
path, ref cmdLines, default, default, false,
|
||||
PROCESS_CREATION_FLAGS.CREATE_SUSPENDED, default, dir, in si, out var pi
|
||||
);
|
||||
pid = pi.dwProcessId;
|
||||
hProc = pi.hProcess;
|
||||
hThread = pi.hThread;
|
||||
return result;
|
||||
}
|
||||
|
||||
// todo: refactor
|
||||
public static unsafe int LoadLibraryAndInject(HANDLE hProc, ReadOnlySpan<char> libPath) {
|
||||
fixed (char* lpModelName = "kernel32.dll") {
|
||||
var hKernel = Native.GetModuleHandle(lpModelName);
|
||||
if (hKernel.IsNull) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("GetModuleHandle fail");
|
||||
}
|
||||
fixed(byte* lpProcName = "LoadLibraryW"u8) {
|
||||
var pLoadLibrary = Native.GetProcAddress(hKernel, (PCSTR)lpProcName);
|
||||
if (pLoadLibrary.IsNull) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("GetProcAddress fail");
|
||||
}
|
||||
var libPathByteLen = (uint) libPath.Length * 2;
|
||||
var pBase = Native.VirtualAllocEx(hProc, default, libPathByteLen + 2, VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT, PAGE_PROTECTION_FLAGS.PAGE_READWRITE);
|
||||
if ((nint)pBase == 0) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("VirtualAllocEx fail");
|
||||
}
|
||||
fixed (void* lpBuffer = libPath) {
|
||||
if (!Native.WriteProcessMemory(hProc, pBase, lpBuffer, libPathByteLen)) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("WriteProcessMemory fail");
|
||||
}
|
||||
}
|
||||
var lpStartAddress = (delegate* unmanaged[Stdcall]<void*, uint>)pLoadLibrary.Value; //THREAD_START_ROUTINE
|
||||
var hThread = Native.CreateRemoteThread(hProc, default, 0, lpStartAddress, pBase, 0);
|
||||
if (hThread.IsNull) {
|
||||
var e = new Win32Exception();
|
||||
Native.VirtualFreeEx(hProc, pBase, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE);
|
||||
return e.PrintMsgAndReturnErrCode("CreateRemoteThread fail");
|
||||
}
|
||||
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
|
||||
Native.VirtualFreeEx(hProc, pBase, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE);
|
||||
}
|
||||
return !Native.CloseHandle(hThread) ? new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail") : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
YaeAchievement/src/NativeMethods.json
Normal file
6
YaeAchievement/src/NativeMethods.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"className": "Native",
|
||||
"allowMarshaling": false,
|
||||
"public": true
|
||||
}
|
||||
21
YaeAchievement/src/NativeMethods.txt
Normal file
21
YaeAchievement/src/NativeMethods.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
CloseClipboard
|
||||
CreateProcess
|
||||
CreateRemoteThread
|
||||
EmptyClipboard
|
||||
GetConsoleMode
|
||||
GetDC
|
||||
GetDeviceCaps
|
||||
GetModuleHandle
|
||||
GetProcAddress
|
||||
GetStdHandle
|
||||
GlobalLock
|
||||
GlobalUnlock
|
||||
OpenClipboard
|
||||
ResumeThread
|
||||
SetClipboardData
|
||||
SetConsoleMode
|
||||
TerminateProcess
|
||||
VirtualAllocEx
|
||||
VirtualFreeEx
|
||||
WaitForSingleObject
|
||||
WriteProcessMemory
|
||||
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 class PaimonRoot {
|
||||
|
||||
public Dictionary<uint, Dictionary<uint, bool>> Achievement { get; set; } = null!;
|
||||
|
||||
public static PaimonRoot FromNotify(AchievementAllDataNotify ntf) {
|
||||
var info = GlobalVars.AchievementInfo.Items.ToDictionary(pair => pair.Key, pair => pair.Value.Group);
|
||||
return new PaimonRoot {
|
||||
Achievement = ntf.AchievementList
|
||||
.Where(a => a.Status >= AchievementStatus.Finished && info.ContainsKey(a.Id))
|
||||
.GroupBy(a => info[a.Id], a => a.Id)
|
||||
.OrderBy(g => g.Key)
|
||||
.ToDictionary(g => g.Key, g => g.ToDictionary(id => id, _ => true))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(PaimonRoot))]
|
||||
[JsonSourceGenerationOptions(
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class PaimonSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(Outputs.PaimonRoot.FromNotify(ntf), Default.PaimonRoot);
|
||||
}
|
||||
}
|
||||
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 class SeelieRoot {
|
||||
|
||||
public class AchievementFinishStatus {
|
||||
|
||||
public bool Done => true;
|
||||
|
||||
}
|
||||
|
||||
public Dictionary<uint, AchievementFinishStatus> Achievements { get; set; } = null!;
|
||||
|
||||
public static SeelieRoot FromNotify(AchievementAllDataNotify ntf) => new () {
|
||||
Achievements = ntf.AchievementList
|
||||
.Where(a => a.Status >= AchievementStatus.Finished)
|
||||
.OrderBy(a => a.Id)
|
||||
.ToDictionary(a => a.Id, _ => new AchievementFinishStatus())
|
||||
};
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(SeelieRoot))]
|
||||
[JsonSourceGenerationOptions(
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class SeelieSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(Outputs.SeelieRoot.FromNotify(ntf), Default.SeelieRoot);
|
||||
}
|
||||
}
|
||||
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 class UApplicationInfo {
|
||||
|
||||
public string ExportApp => "YaeAchievement";
|
||||
|
||||
public string ExportAppVersion => GlobalVars.AppVersionName;
|
||||
|
||||
public long ExportTimestamp => DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
|
||||
public string UIAFVersion => "v1.1";
|
||||
|
||||
}
|
||||
|
||||
public class UAchievementInfo {
|
||||
|
||||
public uint Id { get; set; }
|
||||
|
||||
public uint Status { get; set; }
|
||||
|
||||
public uint Current { get; set; }
|
||||
|
||||
public uint Timestamp { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class UIAFRoot {
|
||||
|
||||
public UApplicationInfo Info => new ();
|
||||
|
||||
public IEnumerable<UAchievementInfo> List { get; set; } = null!;
|
||||
|
||||
public static UIAFRoot FromNotify(AchievementAllDataNotify ntf) => new () {
|
||||
List = ntf.AchievementList
|
||||
.Where(a => a.Status >= AchievementStatus.Finished || a.CurrentProgress > 0)
|
||||
.Select(a => new UAchievementInfo {
|
||||
Id = a.Id,
|
||||
Status = (uint) a.Status,
|
||||
Current = a.CurrentProgress,
|
||||
Timestamp = a.FinishTimestamp
|
||||
})
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(UIAFRoot))]
|
||||
[JsonSourceGenerationOptions(
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class UIAFSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(Outputs.UIAFRoot.FromNotify(ntf), Default.UIAFRoot);
|
||||
}
|
||||
}
|
||||
138
YaeAchievement/src/Parsers/AchievementAllDataNotify.cs
Normal file
138
YaeAchievement/src/Parsers/AchievementAllDataNotify.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Google.Protobuf;
|
||||
using YaeAchievement.res;
|
||||
|
||||
namespace YaeAchievement.Parsers;
|
||||
|
||||
public enum AchievementStatus {
|
||||
Invalid,
|
||||
Unfinished,
|
||||
Finished,
|
||||
RewardTaken,
|
||||
}
|
||||
|
||||
public class AchievementItem {
|
||||
|
||||
public uint Id { get; init; }
|
||||
public uint TotalProgress { get; init; }
|
||||
public uint CurrentProgress { get; init; }
|
||||
public uint FinishTimestamp { get; init; }
|
||||
public AchievementStatus Status { get; init; }
|
||||
|
||||
}
|
||||
|
||||
public class AchievementAllDataNotify {
|
||||
|
||||
public List<AchievementItem> AchievementList { get; private init; } = [];
|
||||
|
||||
private static AchievementAllDataNotify? Instance { get; set; }
|
||||
|
||||
public static bool OnReceive(BinaryReader reader) {
|
||||
var bytes = reader.ReadBytes(reader.ReadInt32());
|
||||
GlobalVars.AchievementDataCache.Write(bytes);
|
||||
Instance = ParseFrom(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void OnFinish() {
|
||||
if (Instance == null) {
|
||||
throw new ApplicationException("No data received");
|
||||
}
|
||||
Export.Choose(Instance);
|
||||
}
|
||||
|
||||
public static AchievementAllDataNotify ParseFrom(byte[] bytes) {
|
||||
using var stream = new CodedInputStream(bytes);
|
||||
var data = new List<Dictionary<uint, uint>>();
|
||||
var errTimes = 0;
|
||||
try {
|
||||
uint tag;
|
||||
while ((tag = stream.ReadTag()) != 0) {
|
||||
if ((tag & 7) == 2) { // is LengthDelimited
|
||||
var dict = new Dictionary<uint, uint>();
|
||||
using var eStream = stream.ReadLengthDelimitedAsStream();
|
||||
try {
|
||||
while ((tag = eStream.ReadTag()) != 0) {
|
||||
if ((tag & 7) != 0) { // not VarInt
|
||||
dict = null;
|
||||
break;
|
||||
}
|
||||
dict[tag >> 3] = eStream.ReadUInt32();
|
||||
}
|
||||
if (dict != null) {
|
||||
data.Add(dict);
|
||||
}
|
||||
} catch (InvalidProtocolBufferException) {
|
||||
if (errTimes++ > 0) { // allows 1 fail on 'reward_taken_goal_id_list'
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InvalidProtocolBufferException) {
|
||||
// ReSharper disable once LocalizableElement
|
||||
Console.WriteLine("Parse failed");
|
||||
File.WriteAllBytes("achievement_raw_data.bin", bytes);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
if (data.Count == 0) {
|
||||
return new AchievementAllDataNotify();
|
||||
}
|
||||
uint tId, sId, iId, currentId, totalId;
|
||||
if (data.Count > 20) { /* uwu */
|
||||
(tId, var cnt) = data // ↓ 2020-09-15 04:15:14
|
||||
.GroupKeys(value => value > 1600114514).Select(g => (g.Key, g.Count())).MaxBy(p => p.Item2);
|
||||
sId = data // FINISHED ↓ ↓ REWARD_TAKEN
|
||||
.GroupKeys(value => value is 2 or 3).First(g => g.Count() == cnt).Key;
|
||||
iId = data // ↓ id: 8xxxx
|
||||
.GroupKeys(value => value / 10000 % 10 == 8).MaxBy(g => g.Count())!.Key;
|
||||
(currentId, totalId) = data
|
||||
.Where(d => d[sId] is 2 or 3)
|
||||
.Select(d => d.ToDictionary().RemoveValues(tId, sId, iId).ToArray())
|
||||
.Where(d => d.Length == 2 && d[0].Value != d[1].Value)
|
||||
.GroupBy(a => a[0].Value > a[1].Value ? (a[0].Key, a[1].Key) : (a[1].Key, a[0].Key))
|
||||
.Select(g => (FieldIds: g.Key, Count: g.Count()))
|
||||
.MaxBy(p => p.Count)
|
||||
.FieldIds;
|
||||
#if DEBUG
|
||||
// ReSharper disable once LocalizableElement
|
||||
Console.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}");
|
||||
#endif
|
||||
} else {
|
||||
var info = GlobalVars.AchievementInfo.PbInfo; // ...
|
||||
iId = info.Id;
|
||||
tId = info.FinishTimestamp;
|
||||
sId = info.Status;
|
||||
totalId = info.TotalProgress;
|
||||
currentId = info.CurrentProgress;
|
||||
if (data.Any(dict => !dict.ContainsKey(iId) || !dict.ContainsKey(sId) || !dict.ContainsKey(totalId))) {
|
||||
Console.WriteLine(App.WaitMetadataUpdate);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
return new AchievementAllDataNotify {
|
||||
AchievementList = data.Select(dict => new AchievementItem {
|
||||
Id = dict[iId],
|
||||
Status = (AchievementStatus) dict[sId],
|
||||
TotalProgress = dict[totalId],
|
||||
CurrentProgress = dict.GetValueOrDefault(currentId),
|
||||
FinishTimestamp = dict.GetValueOrDefault(tId),
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(AchievementAllDataNotify))]
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = true,
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class AchievementRawDataSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(ntf, Default.AchievementAllDataNotify);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
60
YaeAchievement/src/Parsers/PlayerStoreNotify.cs
Normal file
60
YaeAchievement/src/Parsers/PlayerStoreNotify.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Google.Protobuf;
|
||||
using Proto;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
// ReSharper disable CollectionNeverQueried.Global
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
|
||||
|
||||
namespace YaeAchievement.Parsers;
|
||||
|
||||
public class PlayerStoreNotify {
|
||||
|
||||
public uint WeightLimit { get; set; }
|
||||
|
||||
public StoreType StoreType { get; set; }
|
||||
|
||||
public List<Item> ItemList { get; set; } = [];
|
||||
|
||||
public static PlayerStoreNotify Instance { get; } = new ();
|
||||
|
||||
public static bool OnReceive(BinaryReader reader) {
|
||||
var bytes = reader.ReadBytes(reader.ReadInt32());
|
||||
Instance.ParseFrom(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ParseFrom(byte[] bytes) {
|
||||
using var stream = new CodedInputStream(bytes);
|
||||
try {
|
||||
uint tag;
|
||||
while ((tag = stream.ReadTag()) != 0) {
|
||||
var wireType = tag & 7;
|
||||
switch (wireType) {
|
||||
case 0: { // is VarInt
|
||||
var value = stream.ReadUInt32();
|
||||
if (value < 10) {
|
||||
StoreType = (StoreType) value;
|
||||
} else {
|
||||
WeightLimit = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 2: { // is LengthDelimited
|
||||
using var eStream = stream.ReadLengthDelimitedAsStream();
|
||||
while (eStream.PeekTag() != 0) {
|
||||
ItemList.Add(Item.Parser.ParseFrom(eStream));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InvalidProtocolBufferException) {
|
||||
// ReSharper disable once LocalizableElement
|
||||
Console.WriteLine("Parse failed");
|
||||
File.WriteAllBytes("store_raw_data.bin", bytes);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
76
YaeAchievement/src/Program.cs
Normal file
76
YaeAchievement/src/Program.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.Console;
|
||||
using YaeAchievement.Parsers;
|
||||
using YaeAchievement.res;
|
||||
using static YaeAchievement.Utils;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
internal static class Program {
|
||||
|
||||
public static async Task Main(string[] args) {
|
||||
|
||||
if (!new Mutex(true, @"Global\YaeMiku~uwu").WaitOne(0, false)) {
|
||||
Console.WriteLine(App.AnotherInstance);
|
||||
Environment.Exit(302);
|
||||
}
|
||||
|
||||
InstallExitHook();
|
||||
InstallExceptionHook();
|
||||
|
||||
CheckGenshinIsRunning();
|
||||
|
||||
Console.WriteLine(@"----------------------------------------------------");
|
||||
Console.WriteLine(App.AppBanner, GlobalVars.AppVersionName);
|
||||
Console.WriteLine(@"https://github.com/HolographicHat/YaeAchievement");
|
||||
Console.WriteLine(@"----------------------------------------------------");
|
||||
|
||||
AppConfig.Load(args.GetOrNull(0) ?? "auto");
|
||||
Export.ExportTo = ToUIntOrNull(args.GetOrNull(1)) ?? uint.MaxValue;
|
||||
|
||||
await CheckUpdate(ToBooleanOrFalse(args.GetOrNull(2)));
|
||||
|
||||
var historyCache = GlobalVars.AchievementDataCache;
|
||||
|
||||
AchievementAllDataNotify? data = null;
|
||||
try {
|
||||
data = AchievementAllDataNotify.ParseFrom(historyCache.Read().Content.ToByteArray());
|
||||
} catch (Exception) { /* ignored */ }
|
||||
|
||||
if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null) {
|
||||
Console.WriteLine(App.UsePreviousData);
|
||||
if (Console.ReadLine()?.ToUpper() is "Y" or "YES") {
|
||||
Export.Choose(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
StartAndWaitResult(AppConfig.GamePath, new Dictionary<byte, Func<BinaryReader, bool>> {
|
||||
{ 1, AchievementAllDataNotify.OnReceive },
|
||||
{ 2, PlayerStoreNotify.OnReceive },
|
||||
{ 100, PlayerPropNotify.OnReceive },
|
||||
}, () => {
|
||||
#if DEBUG
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
[ModuleInitializer]
|
||||
internal static unsafe void SetupConsole() {
|
||||
var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
|
||||
CONSOLE_MODE mode = default;
|
||||
Native.GetConsoleMode(handle, &mode);
|
||||
Native.SetConsoleMode(handle, mode & ~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE);
|
||||
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;
|
||||
}
|
||||
|
||||
}
|
||||
39
YaeAchievement/src/Utilities/CacheFile.cs
Normal file
39
YaeAchievement/src/Utilities/CacheFile.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.IO.Compression;
|
||||
using Google.Protobuf;
|
||||
using Proto;
|
||||
|
||||
namespace YaeAchievement.Utilities;
|
||||
|
||||
public class CacheFile(string identifier) {
|
||||
|
||||
private readonly string _cacheName = Path.Combine(GlobalVars.CachePath, $"{identifier.MD5Hash()[..16]}.miko");
|
||||
private CacheItem? _content;
|
||||
|
||||
public DateTime LastWriteTime => Exists() ? File.GetLastWriteTimeUtc(_cacheName) : DateTime.UnixEpoch;
|
||||
|
||||
public bool Exists() => File.Exists(_cacheName);
|
||||
|
||||
public CacheItem Read() {
|
||||
if (_content == null) {
|
||||
using var fInput = File.OpenRead(_cacheName);
|
||||
using var dInput = new GZipStream(fInput, CompressionMode.Decompress);
|
||||
_content = CacheItem.Parser.ParseFrom(dInput);
|
||||
}
|
||||
return _content;
|
||||
}
|
||||
|
||||
public void Write(string data, string? etag = null) => Write(ByteString.CopyFromUtf8(data), data.MD5Hash(), etag);
|
||||
|
||||
public void Write(byte[] data, string? etag = null) => Write(ByteString.CopyFrom(data), data.MD5Hash(), etag);
|
||||
|
||||
private void Write(ByteString data, string hash, string? etag) {
|
||||
using var fOut = File.OpenWrite(_cacheName);
|
||||
using var cOut = new GZipStream(fOut, CompressionLevel.SmallestSize);
|
||||
new CacheItem {
|
||||
Etag = etag ?? string.Empty,
|
||||
Version = 3,
|
||||
Checksum = hash,
|
||||
Content = data
|
||||
}.WriteTo(cOut);
|
||||
}
|
||||
}
|
||||
16
YaeAchievement/src/Utilities/Extensions/Collection.cs
Normal file
16
YaeAchievement/src/Utilities/Extensions/Collection.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace System.Collections.Generic;
|
||||
|
||||
public static class Collection {
|
||||
|
||||
public static IDictionary<TKey, TValue> RemoveValues<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dictionary,
|
||||
params TKey[] keys
|
||||
) {
|
||||
foreach (var key in keys) {
|
||||
dictionary.Remove(key);
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
}
|
||||
14
YaeAchievement/src/Utilities/Extensions/Enumerable.cs
Normal file
14
YaeAchievement/src/Utilities/Extensions/Enumerable.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
|
||||
namespace System.Linq;
|
||||
|
||||
public static class Enumerable {
|
||||
|
||||
public static IEnumerable<IGrouping<TKey, TKey>> GroupKeys<TKey, TValue>(
|
||||
this IEnumerable<Dictionary<TKey, TValue>> source,
|
||||
Func<TValue, bool> condition
|
||||
) where TKey : notnull {
|
||||
return source.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key)).GroupBy(x => x);
|
||||
}
|
||||
|
||||
}
|
||||
16
YaeAchievement/src/Utilities/Extensions/Stream.cs
Normal file
16
YaeAchievement/src/Utilities/Extensions/Stream.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// ReSharper disable CheckNamespace
|
||||
|
||||
namespace Google.Protobuf;
|
||||
|
||||
public static class CodedInputStreamExtensions {
|
||||
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Method)]
|
||||
private static extern byte[] ReadRawBytes(CodedInputStream stream, int size);
|
||||
|
||||
public static CodedInputStream ReadLengthDelimitedAsStream(this CodedInputStream stream) {
|
||||
return new CodedInputStream(ReadRawBytes(stream, stream.ReadLength()));
|
||||
}
|
||||
|
||||
}
|
||||
243
YaeAchievement/src/Utils.cs
Normal file
243
YaeAchievement/src/Utils.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipes;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Proto;
|
||||
using YaeAchievement.res;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class Utils {
|
||||
|
||||
public static readonly HttpClient CHttpClient = new (new HttpClientHandler {
|
||||
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
|
||||
}) {
|
||||
DefaultRequestHeaders = {
|
||||
UserAgent = {
|
||||
new ProductInfoHeaderValue("YaeAchievement", GlobalVars.AppVersion.ToString(2))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static async Task<byte[]> GetBucketFile(string path, bool cache = true) {
|
||||
try {
|
||||
return await await Task.WhenAny(GetFile(GlobalVars.RinBucketHost), GetFile(GlobalVars.SakuraBucketHost));
|
||||
} catch (Exception e) when(e is SocketException or TaskCanceledException) {
|
||||
Console.WriteLine(App.NetworkError, e.Message);
|
||||
Environment.Exit(-1);
|
||||
return null!;
|
||||
}
|
||||
async Task<byte[]> GetFile(string host) {
|
||||
using var msg = new HttpRequestMessage();
|
||||
msg.Method = HttpMethod.Get;
|
||||
msg.RequestUri = new Uri($"{host}/{path}");
|
||||
var cacheFile = new CacheFile(path);
|
||||
if (cache && cacheFile.Exists()) {
|
||||
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cacheFile.Read().Etag}");
|
||||
}
|
||||
using var response = await CHttpClient.SendAsync(msg);
|
||||
if (cache && response.StatusCode == HttpStatusCode.NotModified) {
|
||||
return cacheFile.Read().Content.ToByteArray();
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
var responseBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
if (cache) {
|
||||
var etag = response.Headers.ETag!.Tag;
|
||||
cacheFile.Write(responseBytes, etag);
|
||||
}
|
||||
return responseBytes;
|
||||
}
|
||||
}
|
||||
|
||||
public static T? GetOrNull<T>(this T[] array, uint index) where T : class {
|
||||
return array.Length > index ? array[index] : null;
|
||||
}
|
||||
|
||||
public static uint? ToUIntOrNull(string? value) {
|
||||
return value != null ? uint.TryParse(value, out var result) ? result : null : null;
|
||||
}
|
||||
|
||||
public static bool ToBooleanOrFalse(string? value) {
|
||||
return value != null && bool.TryParse(value, out var result) && result;
|
||||
}
|
||||
|
||||
public static unsafe void CopyToClipboard(string text) {
|
||||
if (Native.OpenClipboard(HWND.Null)) {
|
||||
Native.EmptyClipboard();
|
||||
var hGlobal = (HGLOBAL) Marshal.AllocHGlobal((text.Length + 1) * 2);
|
||||
var hPtr = (nint) Native.GlobalLock(hGlobal);
|
||||
Marshal.Copy(text.ToCharArray(), 0, hPtr, text.Length);
|
||||
Native.GlobalUnlock((HGLOBAL) hPtr);
|
||||
Native.SetClipboardData(13, new HANDLE(hPtr));
|
||||
Marshal.FreeHGlobal(hGlobal);
|
||||
Native.CloseClipboard();
|
||||
} else {
|
||||
throw new Win32Exception();
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private static UpdateInfo _updateInfo = null!;
|
||||
|
||||
public static async Task CheckUpdate(bool useLocalLib) {
|
||||
var info = UpdateInfo.Parser.ParseFrom(await GetBucketFile("schicksal/version"))!;
|
||||
if (GlobalVars.AppVersionCode < info.VersionCode) {
|
||||
Console.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, info.VersionName);
|
||||
Console.WriteLine(App.UpdateDescription, info.Description);
|
||||
if (info.EnableAutoUpdate) {
|
||||
Console.WriteLine(App.UpdateDownloading);
|
||||
var tmpPath = Path.GetTempFileName();
|
||||
await File.WriteAllBytesAsync(tmpPath, await GetBucketFile(info.PackageLink));
|
||||
var updaterArgs = $"{Environment.ProcessId}|{Environment.ProcessPath}|{tmpPath}";
|
||||
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
|
||||
await File.WriteAllBytesAsync(updaterPath, App.Updater);
|
||||
ShellOpen(updaterPath, updaterArgs.ToBytes().ToBase64());
|
||||
GlobalVars.PauseOnExit = false;
|
||||
Environment.Exit(0);
|
||||
}
|
||||
Console.WriteLine(App.DownloadLink, info.PackageLink);
|
||||
if (info.ForceUpdate) {
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
if (info.EnableLibDownload && !useLocalLib) {
|
||||
var data = await GetBucketFile("schicksal/lic.dll");
|
||||
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data);
|
||||
}
|
||||
_updateInfo = info;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMethodReturnValue.Global
|
||||
public static bool ShellOpen(string path, string? args = null) {
|
||||
try {
|
||||
var startInfo = new ProcessStartInfo {
|
||||
FileName = path,
|
||||
UseShellExecute = true
|
||||
};
|
||||
if (args != null) {
|
||||
startInfo.Arguments = args;
|
||||
}
|
||||
return new Process {
|
||||
StartInfo = startInfo
|
||||
}.Start();
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void CheckGenshinIsRunning() {
|
||||
Process.EnterDebugMode();
|
||||
foreach (var process in Process.GetProcesses()) {
|
||||
if (process.ProcessName is "GenshinImpact" or "YuanShen"
|
||||
&& !process.HasExited
|
||||
&& process.MainWindowHandle != nint.Zero
|
||||
) {
|
||||
Console.WriteLine(App.GenshinIsRunning, process.Id);
|
||||
Environment.Exit(301);
|
||||
}
|
||||
}
|
||||
Process.LeaveDebugMode();
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static Process? proc;
|
||||
|
||||
public static void InstallExitHook() {
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
|
||||
proc?.Kill();
|
||||
if (GlobalVars.PauseOnExit) {
|
||||
Console.WriteLine(App.PressKeyToExit);
|
||||
Console.ReadKey();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void InstallExceptionHook() {
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e) => {
|
||||
var ex = e.ExceptionObject;
|
||||
switch (ex) {
|
||||
case ApplicationException ex1:
|
||||
Console.WriteLine(ex1.Message);
|
||||
break;
|
||||
case SocketException ex2:
|
||||
Console.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
|
||||
break;
|
||||
case HttpRequestException ex3:
|
||||
Console.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine(ex.ToString());
|
||||
break;
|
||||
}
|
||||
Environment.Exit(-1);
|
||||
};
|
||||
}
|
||||
|
||||
private static bool _isUnexpectedExit;
|
||||
|
||||
// ReSharper disable once UnusedMethodReturnValue.Global
|
||||
public static Thread StartAndWaitResult(string exePath, Dictionary<byte, Func<BinaryReader, bool>> handlers, Action onFinish) {
|
||||
if (!Injector.CreateProcess(exePath, out var hProcess, out var hThread, out var pid)) {
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("ICreateProcess fail"));
|
||||
}
|
||||
if (Injector.LoadLibraryAndInject(hProcess,GlobalVars.LibFilePath.AsSpan()) != 0)
|
||||
{
|
||||
if (!Native.TerminateProcess(hProcess, 0))
|
||||
{
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail"));
|
||||
}
|
||||
}
|
||||
Console.WriteLine(App.GameLoading, pid);
|
||||
proc = Process.GetProcessById(Convert.ToInt32(pid));
|
||||
proc.EnableRaisingEvents = true;
|
||||
proc.Exited += (_, _) => {
|
||||
if (_isUnexpectedExit)
|
||||
{
|
||||
proc = null;
|
||||
Console.WriteLine(App.GameProcessExit);
|
||||
Environment.Exit(114514);
|
||||
}
|
||||
};
|
||||
if (Native.ResumeThread(hThread) == 0xFFFFFFFF)
|
||||
{
|
||||
var e = new Win32Exception();
|
||||
if (!Native.TerminateProcess(hProcess, 0))
|
||||
{
|
||||
new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail");
|
||||
}
|
||||
Environment.Exit(e.PrintMsgAndReturnErrCode("ResumeThread fail"));
|
||||
}
|
||||
if (!Native.CloseHandle(hProcess))
|
||||
{
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail"));
|
||||
}
|
||||
|
||||
var ts = new ThreadStart(() => {
|
||||
var server = new NamedPipeServerStream(GlobalVars.PipeName);
|
||||
server.WaitForConnection();
|
||||
using var reader = new BinaryReader(server);
|
||||
while (!proc.HasExited) {
|
||||
var type = reader.ReadByte();
|
||||
if (type == 0xFF) {
|
||||
_isUnexpectedExit = false;
|
||||
onFinish();
|
||||
break;
|
||||
}
|
||||
if (handlers.TryGetValue(type, out var handler)) {
|
||||
if (handler(reader)) {
|
||||
handlers.Remove(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var th = new Thread(ts);
|
||||
th.Start();
|
||||
return th;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
149
app.js
149
app.js
@@ -1,149 +0,0 @@
|
||||
const zlib = require("zlib")
|
||||
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 } = require("./utils")
|
||||
const { exportData } = require("./export")
|
||||
const { exitHook } = require("./exitHook.js");
|
||||
|
||||
// TODO: use kotlin rewrite it
|
||||
(async () => {
|
||||
try {
|
||||
exitHook(() => {
|
||||
setupHost(true)
|
||||
console.log("按任意键退出")
|
||||
cp.execSync("pause > nul", { stdio: "inherit" })
|
||||
})
|
||||
appcenter.init()
|
||||
let conf = await initConfig()
|
||||
try {
|
||||
cp.execSync("net session", { stdio: "ignore" })
|
||||
} catch (e) {
|
||||
console.log("\x1b[91m请使用管理员身份运行此程序\x1b[0m")
|
||||
return
|
||||
}
|
||||
await checkUpdate()
|
||||
checkCDN().then(_ => debug("CDN check success."))
|
||||
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
|
||||
}
|
||||
})
|
||||
gameProcess.on("exit", () => {
|
||||
if (unexpectedExit) 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;
|
||||
const createMonitor = () => {
|
||||
monitor = setInterval(async () => {
|
||||
if (login && lastRecvTimestamp + 2 < parseInt(Date.now() / 1000)) {
|
||||
unexpectedExit = false
|
||||
server.close()
|
||||
hServer.close()
|
||||
gameProcess.kill()
|
||||
clearInterval(monitor)
|
||||
setupHost(true)
|
||||
console.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 = zlib.brotliCompressSync(merged)
|
||||
const response = await upload(compressed)
|
||||
if (response.status !== 200) {
|
||||
log(`发生错误: ${response.data.toString()}`)
|
||||
log(`请求ID: ${response.headers["x-api-requestid"]}`)
|
||||
log("请联系开发者以获取帮助")
|
||||
} else {
|
||||
const data = zlib.brotliDecompressSync(response.data)
|
||||
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("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
|
||||
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(() => console.log("加载完毕"))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (e instanceof Error) {
|
||||
appcenter.uploadError(e, true)
|
||||
} else {
|
||||
appcenter.uploadError(Error(e), true)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
130
appcenter.js
130
appcenter.js
@@ -1,130 +0,0 @@
|
||||
const cp = require("child_process")
|
||||
const axios = require("axios")
|
||||
const crypto = require("crypto")
|
||||
const { version } = require("./version")
|
||||
|
||||
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 readRegistry = (path, key) => {
|
||||
const i = cp.execSync(`reg query "${path}" /v ${key}`, {
|
||||
encoding: "utf-8"
|
||||
}).split("\n")[2].split(" ").filter(s => s.length > 0).map(s => s.trim())
|
||||
switch (i[1]) {
|
||||
case "REG_SZ":
|
||||
return i[2]
|
||||
case "REG_DWORD":
|
||||
return parseInt(i[2])
|
||||
default:
|
||||
throw "Unsupported"
|
||||
}
|
||||
}
|
||||
|
||||
const queue = []
|
||||
const session = crypto.randomUUID()
|
||||
const key = "648b83bf-d439-49bd-97f4-e1e506bdfe39"
|
||||
|
||||
const install = (() => {
|
||||
const s = readRegistry("HKCU\\SOFTWARE\\miHoYoSDK", "MIHOYOSDK_DEVICE_ID")
|
||||
return `${s.substring(0, 8)}-${s.substring(8, 12)}-${s.substring(12, 16)}-${s.substring(16, 20)}-${s.substring(20, 32)}`
|
||||
})()
|
||||
|
||||
const device = (() => {
|
||||
const csi = cp.execSync("wmic computersystem get manufacturer,model /format:csv", {
|
||||
encoding: "utf-8"
|
||||
}).split("\n")[2].split(",").map(s => s.trim())
|
||||
const osi = cp.execSync("wmic os get currentTimeZone, version /format:csv", {
|
||||
encoding: "utf-8"
|
||||
}).split("\n")[2].split(",").map(s => s.trim())
|
||||
return {
|
||||
model: csi[2],
|
||||
oemName: csi[1],
|
||||
timeZoneOffset: parseInt(osi[1]),
|
||||
osBuild: `${osi[2]}.${readRegistry("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "UBR")}`,
|
||||
osVersion: osi[2],
|
||||
locale: readRegistry("HKCU\\Control Panel\\International", "LocaleName"),
|
||||
carrierCountry: readRegistry("HKCU\\Control Panel\\International\\Geo", "Name"),
|
||||
sdkName: "appcenter.wpf.netcore",
|
||||
sdkVersion: "4.5.0",
|
||||
osName: "WINDOWS",
|
||||
appVersion: version.name,
|
||||
appBuild: version.code,
|
||||
appNamespace: "default"
|
||||
}
|
||||
})()
|
||||
|
||||
const upload = () => {
|
||||
if (queue.length > 0) {
|
||||
try {
|
||||
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 (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
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 init = () => {
|
||||
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 = {
|
||||
init, upload, uploadError
|
||||
}
|
||||
BIN
cert/root.p12
BIN
cert/root.p12
Binary file not shown.
50
exitHook.js
50
exitHook.js
@@ -1,50 +0,0 @@
|
||||
// https://github.com/sindresorhus/exit-hook
|
||||
|
||||
const callbacks = new Set();
|
||||
let isCalled = false;
|
||||
let isRegistered = false;
|
||||
|
||||
function exit(shouldManuallyExit, signal) {
|
||||
if (isCalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
isCalled = true;
|
||||
|
||||
for (const callback of callbacks) {
|
||||
callback();
|
||||
}
|
||||
|
||||
if (shouldManuallyExit === true) {
|
||||
process.exit(128 + signal);
|
||||
}
|
||||
}
|
||||
|
||||
function exitHook(onExit) {
|
||||
callbacks.add(onExit);
|
||||
|
||||
if (!isRegistered) {
|
||||
isRegistered = true;
|
||||
|
||||
process.once('exit', exit);
|
||||
process.once('SIGINT', exit.bind(undefined, true, 2));
|
||||
process.once('SIGTERM', exit.bind(undefined, true, 15));
|
||||
|
||||
// PM2 Cluster shutdown message. Caught to support async handlers with pm2, needed because
|
||||
// explicitly calling process.exit() doesn't trigger the beforeExit event, and the exit
|
||||
// event cannot support async handlers, since the event loop is never called after it.
|
||||
process.on('message', message => {
|
||||
if (message === 'shutdown') {
|
||||
exit(true, -128);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
callbacks.delete(onExit);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exitHook
|
||||
}
|
||||
130
export.js
130
export.js
@@ -1,130 +0,0 @@
|
||||
const fs = require("fs")
|
||||
const readline = require("readline")
|
||||
const { exec } = require("child_process")
|
||||
const { loadCache } = require("./utils")
|
||||
|
||||
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))
|
||||
console.log(`导出为文件: ${fp}`)
|
||||
}
|
||||
|
||||
const exportToPaimon = async proto => {
|
||||
const out = { achievements: {} }
|
||||
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.achievements[gid] === undefined) {
|
||||
out.achievements[gid] = {}
|
||||
}
|
||||
out.achievements[gid][id] = true
|
||||
})
|
||||
const fp = `./export-${Date.now()}-paimon.json`
|
||||
fs.writeFileSync(fp, JSON.stringify(out))
|
||||
console.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)
|
||||
})
|
||||
})
|
||||
exec("clip").stdin.end(JSON.stringify(out,null,2))
|
||||
console.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())}`
|
||||
}
|
||||
proto.list.forEach(({current, finishTimestamp, id, status, require}) => {
|
||||
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")}`)
|
||||
console.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> ")
|
||||
rl.close()
|
||||
switch (chosen) {
|
||||
case "0":
|
||||
await exportToCocogoat(proto)
|
||||
break
|
||||
case "1":
|
||||
await exportToPaimon(proto)
|
||||
break
|
||||
case "2":
|
||||
await exportToSeelie(proto)
|
||||
break
|
||||
default:
|
||||
await exportToCsv(proto)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exportData
|
||||
}
|
||||
4
lib/.gitignore
vendored
Normal file
4
lib/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.vs
|
||||
build
|
||||
YaeAchievementLib.vcxproj.user
|
||||
YaeAchievementLib.vcxproj.filters
|
||||
17
lib/YaeAchievementLib.nuspec
Normal file
17
lib/YaeAchievementLib.nuspec
Normal file
@@ -0,0 +1,17 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Yae.Lib</id>
|
||||
<version>5.3.1</version>
|
||||
<authors>HolographicHat</authors>
|
||||
<developmentDependency>true</developmentDependency>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<license type="expression">GPL-3.0-only</license>
|
||||
<licenseUrl>https://licenses.nuget.org/GPL-3.0-only</licenseUrl>
|
||||
<projectUrl>https://github.com/HolographicHat/Yae</projectUrl>
|
||||
<description>Yae Lib</description>
|
||||
<repository type="git" url="https://github.com/HolographicHat/Yae" commit="$commit$" />
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="build\x64\Release\YaeLib.dll" target="runtimes\win-x64\native" />
|
||||
</files>
|
||||
</package>
|
||||
29
lib/YaeAchievementLib.sln
Normal file
29
lib/YaeAchievementLib.sln
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32407.343
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "YaeAchievementLib", "YaeAchievementLib.vcxproj", "{83C3DF1A-6219-408E-98A3-C7040CCC96FD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x64.Build.0 = Debug|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x64.ActiveCfg = Release|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x64.Build.0 = Release|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x86.ActiveCfg = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {470905A4-E6C4-4363-B44D-BAE9A50755A3}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
114
lib/YaeAchievementLib.vcxproj
Normal file
114
lib/YaeAchievementLib.vcxproj
Normal file
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{83c3df1a-6219-408e-98a3-c7040ccc96fd}</ProjectGuid>
|
||||
<RootNamespace>YaeAchievementLib</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>YaeLib</TargetName>
|
||||
<GenerateManifest>false</GenerateManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
|
||||
<GenerateManifest>false</GenerateManifest>
|
||||
<TargetName>YaeLib</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>NotSet</SubSystem>
|
||||
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PostBuildEvent />
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>_AMD64_;NDEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<LanguageStandard_C>stdc11</LanguageStandard_C>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>NotSet</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PostBuildEvent />
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\globals.h" />
|
||||
<ClInclude Include="src\il2cpp-types.h" />
|
||||
<ClInclude Include="src\il2cpp-init.h" />
|
||||
<ClInclude Include="src\NamedPipe.h" />
|
||||
<ClInclude Include="src\ntprivate.h" />
|
||||
<ClInclude Include="src\util.h" />
|
||||
<ClInclude Include="src\Zydis.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\dllmain.cpp" />
|
||||
<ClCompile Include="src\il2cpp-init.cpp" />
|
||||
<ClCompile Include="src\util.cpp" />
|
||||
<ClCompile Include="src\Zydis.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
46
lib/src/NamedPipe.h
Normal file
46
lib/src/NamedPipe.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <span>
|
||||
|
||||
template <typename T>
|
||||
concept IsSpan = requires(T t) {
|
||||
{ t.data() } -> std::convertible_to<const void*>;
|
||||
{ t.size() } -> std::convertible_to<std::size_t>;
|
||||
{ t.size_bytes() } -> std::convertible_to<std::size_t>;
|
||||
};
|
||||
|
||||
class NamedPipe
|
||||
{
|
||||
HANDLE m_hPipe = INVALID_HANDLE_VALUE;
|
||||
public:
|
||||
NamedPipe(HANDLE hPipe) : m_hPipe(hPipe) {}
|
||||
~NamedPipe() { if (m_hPipe != INVALID_HANDLE_VALUE) CloseHandle(m_hPipe); }
|
||||
|
||||
operator HANDLE() const { return m_hPipe; }
|
||||
operator bool() const { return m_hPipe != INVALID_HANDLE_VALUE && m_hPipe != nullptr; }
|
||||
NamedPipe& operator= (HANDLE hPipe) {
|
||||
m_hPipe = hPipe;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Write(const void* data, size_t size) const
|
||||
{
|
||||
DWORD bytesWritten;
|
||||
if (!WriteFile(m_hPipe, data, static_cast<DWORD>(size), &bytesWritten, nullptr) || bytesWritten != size)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <IsSpan T>
|
||||
bool Write(const T& data) const
|
||||
{
|
||||
return Write(data.data(), data.size_bytes());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool Write(const T& data) const
|
||||
{
|
||||
return Write(&data, sizeof(T));
|
||||
}
|
||||
|
||||
};
|
||||
54990
lib/src/Zydis.c
Normal file
54990
lib/src/Zydis.c
Normal file
File diff suppressed because one or more lines are too long
12113
lib/src/Zydis.h
Normal file
12113
lib/src/Zydis.h
Normal file
File diff suppressed because it is too large
Load Diff
274
lib/src/dllmain.cpp
Normal file
274
lib/src/dllmain.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
// ReSharper disable CppClangTidyCertErr33C
|
||||
#include <Windows.h>
|
||||
#include <print>
|
||||
#include <string>
|
||||
#include <future>
|
||||
#include <TlHelp32.h>
|
||||
|
||||
#include "globals.h"
|
||||
#include "util.h"
|
||||
#include "il2cpp-init.h"
|
||||
#include "il2cpp-types.h"
|
||||
#include "ntprivate.h"
|
||||
|
||||
CRITICAL_SECTION CriticalSection;
|
||||
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index);
|
||||
|
||||
namespace
|
||||
{
|
||||
PacketType GetPacketType(const PacketMeta* packet)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto cmdid = packet->CmdId;
|
||||
|
||||
if (AchievementId && cmdid == AchievementId)
|
||||
return PacketType::Achivement;
|
||||
|
||||
if (AchievementIdSet.contains(cmdid) && packet->DataLength > 500)
|
||||
return PacketType::Achivement;
|
||||
|
||||
if (PlayerStoreId && cmdid == PlayerStoreId)
|
||||
return PacketType::Inventory;
|
||||
|
||||
return PacketType::None;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Hook {
|
||||
|
||||
|
||||
uint16_t __fastcall BitConverter_ToUInt16(Array<uint8_t>* val, const int startIndex)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto ToUInt16 = reinterpret_cast<decltype(&BitConverter_ToUInt16)>(Offset.BitConverter_ToUInt16);
|
||||
|
||||
EnterCriticalSection(&CriticalSection);
|
||||
SetBreakpoint((HANDLE)-2, 0, false, 0);
|
||||
const auto ret = ToUInt16(val, startIndex);
|
||||
SetBreakpoint((HANDLE)-2, Offset.BitConverter_ToUInt16, true, 0);
|
||||
LeaveCriticalSection(&CriticalSection);
|
||||
|
||||
if (ret != 0xAB89)
|
||||
return ret;
|
||||
|
||||
const auto packet = val->As<PacketMeta*>();
|
||||
const auto packetType = GetPacketType(packet);
|
||||
if (packetType == PacketType::None)
|
||||
return ret;
|
||||
|
||||
#ifdef _DEBUG
|
||||
std::println("PacketType: {}", static_cast<uint8_t>(packetType));
|
||||
std::println("CmdId: {}", packet->CmdId);
|
||||
std::println("DataLength: {}", packet->DataLength);
|
||||
//std::println("Data: {}", Util::Base64Encode(packet->AsSpan()));
|
||||
#endif
|
||||
|
||||
if (!MessagePipe.Write(packetType))
|
||||
Util::Win32ErrorDialog(1002, GetLastError());
|
||||
|
||||
if (!MessagePipe.Write(packet->DataLength))
|
||||
Util::Win32ErrorDialog(1003, GetLastError());
|
||||
|
||||
if (!MessagePipe.Write(packet->AsSpan()))
|
||||
Util::Win32ErrorDialog(1004, GetLastError());
|
||||
|
||||
if (!AchievementsWritten)
|
||||
AchievementsWritten = packetType == PacketType::Achivement;
|
||||
|
||||
if (!PlayerStoreWritten)
|
||||
PlayerStoreWritten = packetType == PacketType::Inventory;
|
||||
|
||||
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
|
||||
{
|
||||
if (!MessagePipe.Write(PacketType::End))
|
||||
Util::Win32ErrorDialog(9001, GetLastError());
|
||||
#ifdef _DEBUG
|
||||
system("pause");
|
||||
#endif
|
||||
ExitProcess(0);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void __fastcall AccountDataItem_UpdateNormalProp(const void* __this, const int type, const double value, const double lastValue, const int state)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto UpdateNormalProp = reinterpret_cast<decltype(&AccountDataItem_UpdateNormalProp)>(Offset.AccountDataItem_UpdateNormalProp);
|
||||
|
||||
EnterCriticalSection(&CriticalSection);
|
||||
SetBreakpoint((HANDLE)-2, 0, false, 1);
|
||||
UpdateNormalProp(__this, type, value, lastValue, state);
|
||||
SetBreakpoint((HANDLE)-2, Offset.AccountDataItem_UpdateNormalProp, true, 1);
|
||||
LeaveCriticalSection(&CriticalSection);
|
||||
|
||||
#ifdef _DEBUG
|
||||
std::println("PropType: {}", type);
|
||||
std::println("PropState: {}", state);
|
||||
std::println("PropValue: {}", value);
|
||||
std::println("PropLastValue: {}", lastValue);
|
||||
#endif
|
||||
if (RequiredPlayerProperties.erase(type) != 0)
|
||||
{
|
||||
if (!MessagePipe.Write(PacketType::PropData))
|
||||
Util::Win32ErrorDialog(2002, GetLastError());
|
||||
if (!MessagePipe.Write(type))
|
||||
Util::Win32ErrorDialog(2003, GetLastError());
|
||||
if (!MessagePipe.Write(value))
|
||||
Util::Win32ErrorDialog(2004, GetLastError());
|
||||
}
|
||||
|
||||
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
|
||||
{
|
||||
if (!MessagePipe.Write(PacketType::End))
|
||||
Util::Win32ErrorDialog(9001, GetLastError());
|
||||
#ifdef _DEBUG
|
||||
system("pause");
|
||||
#endif
|
||||
ExitProcess(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LONG __stdcall VectoredExceptionHandler(PEXCEPTION_POINTERS ep)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto exceptionRecord = ep->ExceptionRecord;
|
||||
const auto contextRecord = ep->ContextRecord;
|
||||
|
||||
if (exceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
|
||||
{
|
||||
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.BitConverter_ToUInt16)) {
|
||||
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::BitConverter_ToUInt16);
|
||||
contextRecord->EFlags &= ~0x100; // clear the trap flag
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.AccountDataItem_UpdateNormalProp)) {
|
||||
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::AccountDataItem_UpdateNormalProp);
|
||||
contextRecord->EFlags &= ~0x100; // clear the trap flag
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index)
|
||||
{
|
||||
using namespace Globals;
|
||||
|
||||
if (index > 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
CONTEXT ctx{};
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
GetThreadContext(thread, &ctx);
|
||||
|
||||
DWORD64* dr = &ctx.Dr0;
|
||||
dr[index] = enable ? address : 0;
|
||||
|
||||
const auto mask = 1ull << (index * 2);
|
||||
ctx.Dr7 |= mask;
|
||||
|
||||
SetThreadContext(thread, &ctx);
|
||||
}
|
||||
|
||||
DWORD __stdcall ThreadProc(LPVOID hInstance)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
AllocConsole();
|
||||
freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
|
||||
system("pause");
|
||||
#endif
|
||||
InitializeCriticalSection(&CriticalSection);
|
||||
|
||||
auto initFuture = std::async(std::launch::async, InitIL2CPP);
|
||||
|
||||
using namespace Globals;
|
||||
const auto pid = GetCurrentProcessId();
|
||||
|
||||
while ((GameWindow = Util::FindMainWindowByPID(pid)) == nullptr) {
|
||||
SwitchToThread();
|
||||
}
|
||||
|
||||
if (!initFuture.get())
|
||||
ExitProcess(0);
|
||||
|
||||
MessagePipe = CreateFileA(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (!MessagePipe)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
std::println("CreateFile failed: {}", GetLastError());
|
||||
#else
|
||||
Util::Win32ErrorDialog(1001, GetLastError());
|
||||
ExitProcess(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
AddVectoredExceptionHandler(1, VectoredExceptionHandler);
|
||||
while (true)
|
||||
{
|
||||
THREADENTRY32 te32{};
|
||||
te32.dwSize = sizeof(THREADENTRY32);
|
||||
const auto hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
||||
for (Thread32First(hSnapshot, &te32); Thread32Next(hSnapshot, &te32);)
|
||||
{
|
||||
if (te32.th32OwnerProcessID != pid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID))
|
||||
{
|
||||
EnterCriticalSection(&CriticalSection);
|
||||
SetBreakpoint(hThread, Offset.BitConverter_ToUInt16, true, 0);
|
||||
SetBreakpoint(hThread, Offset.AccountDataItem_UpdateNormalProp, true, 1);
|
||||
CloseHandle(hThread);
|
||||
LeaveCriticalSection(&CriticalSection);
|
||||
}
|
||||
}
|
||||
CloseHandle(hSnapshot);
|
||||
Sleep(1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// DLL entry point
|
||||
BOOL __stdcall DllMain(HMODULE hInstance, DWORD fdwReason, LPVOID lpReserved)
|
||||
{
|
||||
// Check injectee
|
||||
WCHAR szFileName[MAX_PATH]{};
|
||||
DWORD length = 0;
|
||||
GetModuleFileNameW(NULL, szFileName, MAX_PATH);
|
||||
if (!(wcsstr(szFileName, L"YuanShen.exe") || wcsstr(szFileName, L"GenshinImpact.exe")))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (fdwReason == DLL_PROCESS_ATTACH)
|
||||
{
|
||||
if (hInstance)
|
||||
{
|
||||
LdrAddRefDll(LDR_ADDREF_DLL_PIN, hInstance);
|
||||
}
|
||||
|
||||
if (const auto hThread = CreateThread(nullptr, 0, ThreadProc, hInstance, 0, nullptr)) {
|
||||
CloseHandle(hThread);
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static LRESULT WINAPI YaeGetWindowHookImpl(int code, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
return CallNextHookEx(NULL, code, wParam, lParam);
|
||||
}
|
||||
|
||||
EXTERN_C __declspec(dllexport) HRESULT WINAPI YaeGetWindowHook(_Out_ HOOKPROC* pHookProc)
|
||||
{
|
||||
*pHookProc = YaeGetWindowHookImpl;
|
||||
return S_OK;
|
||||
}
|
||||
54
lib/src/globals.h
Normal file
54
lib/src/globals.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <unordered_set>
|
||||
#include "NamedPipe.h"
|
||||
|
||||
#define PROPERTY2(type, name, cn, os) \
|
||||
type name##_cn = cn; \
|
||||
type name##_os = os; \
|
||||
type get_##name() { return Globals::IsCNREL ? name##_cn : name##_os; } \
|
||||
void set_##name(type value) { if (Globals::IsCNREL) name##_cn = value; else name##_os = value; } \
|
||||
__declspec(property(get = get_##name, put = set_##name)) type name;
|
||||
|
||||
namespace Globals
|
||||
{
|
||||
inline HWND GameWindow = nullptr;
|
||||
inline NamedPipe MessagePipe = nullptr;
|
||||
inline bool IsCNREL = true;
|
||||
inline uintptr_t BaseAddress = 0;
|
||||
|
||||
// 5.1.0 - 24082
|
||||
inline uint16_t AchievementId = 0; // use non-zero to override dynamic search
|
||||
inline std::unordered_set<uint16_t> AchievementIdSet;
|
||||
|
||||
// 5.3.0 - 23233
|
||||
inline uint16_t PlayerStoreId = 0; // use non-zero to override dynamic search
|
||||
|
||||
inline bool AchievementsWritten = false;
|
||||
inline bool PlayerStoreWritten = false;
|
||||
|
||||
/*
|
||||
* PROP_PLAYER_HCOIN = 10015,
|
||||
* PROP_PLAYER_WAIT_SUB_HCOIN = 10022,
|
||||
* PROP_PLAYER_SCOIN = 10016,
|
||||
* PROP_PLAYER_WAIT_SUB_SCOIN = 10023,
|
||||
* PROP_PLAYER_MCOIN = 10025,
|
||||
* PROP_PLAYER_WAIT_SUB_MCOIN = 10026,
|
||||
* PROP_PLAYER_HOME_COIN = 10042,
|
||||
* PROP_PLAYER_WAIT_SUB_HOME_COIN = 10043,
|
||||
* PROP_PLAYER_ROLE_COMBAT_COIN = 10053,
|
||||
* PROP_PLAYER_MUSIC_GAME_BOOK_COIN = 10058,
|
||||
*/
|
||||
inline std::unordered_set<int> RequiredPlayerProperties = { 10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058 };
|
||||
|
||||
class Offsets
|
||||
{
|
||||
public:
|
||||
PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0, 0);
|
||||
//PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0x0F826CF0, 0x0F825F10); // use non-zero to override dynamic search
|
||||
PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0, 0);
|
||||
//PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0x0D9FE060, 0x0D94D910); // use non-zero to override dynamic search
|
||||
};
|
||||
|
||||
inline Offsets Offset;
|
||||
}
|
||||
588
lib/src/il2cpp-init.cpp
Normal file
588
lib/src/il2cpp-init.cpp
Normal file
@@ -0,0 +1,588 @@
|
||||
#include <Windows.h>
|
||||
#include <print>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iterator>
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <immintrin.h>
|
||||
|
||||
#include "globals.h"
|
||||
#include "Zydis.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
class DecodedInstruction
|
||||
{
|
||||
public:
|
||||
DecodedInstruction() = default;
|
||||
~DecodedInstruction() = default;
|
||||
DecodedInstruction(const ZydisDecodedInstruction& instruction) : Instruction(instruction) {}
|
||||
DecodedInstruction(const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : Instruction(instruction) {
|
||||
Operands = { operands, operands + operandCount };
|
||||
}
|
||||
DecodedInstruction(const uint32_t rva, const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : RVA(rva), Instruction(instruction) {
|
||||
Operands = { operands, operands + operandCount };
|
||||
}
|
||||
|
||||
// copy constructor
|
||||
DecodedInstruction(const DecodedInstruction& other) = default;
|
||||
|
||||
// move constructor
|
||||
DecodedInstruction(DecodedInstruction&& other) noexcept : RVA(other.RVA), Instruction(other.Instruction), Operands(std::move(other.Operands)) {}
|
||||
|
||||
uint32_t RVA = 0;
|
||||
ZydisDecodedInstruction Instruction;
|
||||
std::vector<ZydisDecodedOperand> Operands;
|
||||
};
|
||||
|
||||
std::span<uint8_t> GetSection(LPCSTR name)
|
||||
{
|
||||
using namespace Globals;
|
||||
if (BaseAddress == 0)
|
||||
return {};
|
||||
|
||||
const auto dosHeader = (PIMAGE_DOS_HEADER)BaseAddress;
|
||||
const auto ntHeader = (PIMAGE_NT_HEADERS)((uintptr_t)dosHeader + dosHeader->e_lfanew);
|
||||
const auto sectionHeader = IMAGE_FIRST_SECTION(ntHeader);
|
||||
|
||||
for (auto i = 0; i < ntHeader->FileHeader.NumberOfSections; i++)
|
||||
{
|
||||
if (strcmp((char*)sectionHeader[i].Name, name) == 0)
|
||||
{
|
||||
const auto sectionSize = sectionHeader[i].Misc.VirtualSize;
|
||||
const auto virtualAddress = BaseAddress + sectionHeader[i].VirtualAddress;
|
||||
return std::span(reinterpret_cast<uint8_t*>(virtualAddress), sectionSize);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// decodes all instruction until next push, ignores branching
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="maxInstructions"></param>
|
||||
/// <returns>std::vector DecodedInstruction</returns>
|
||||
std::vector<DecodedInstruction> DecodeFunction(uintptr_t address, int32_t maxInstructions = -1)
|
||||
{
|
||||
using namespace Globals;
|
||||
|
||||
std::vector<DecodedInstruction> instructions;
|
||||
|
||||
ZydisDecoder decoder{};
|
||||
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
|
||||
|
||||
ZydisDecodedInstruction instruction{};
|
||||
ZydisDecoderContext context{};
|
||||
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT_VISIBLE]{};
|
||||
|
||||
while (true)
|
||||
{
|
||||
const auto data = reinterpret_cast<uint8_t*>(address);
|
||||
auto status = ZydisDecoderDecodeInstruction(&decoder, &context, data, ZYDIS_MAX_INSTRUCTION_LENGTH, &instruction);
|
||||
if (!ZYAN_SUCCESS(status))
|
||||
{
|
||||
// for skipping jump tables
|
||||
address += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
status = ZydisDecoderDecodeOperands(&decoder, &context, &instruction, operands, instruction.operand_count_visible);
|
||||
if (!ZYAN_SUCCESS(status))
|
||||
{
|
||||
// for skipping jump tables
|
||||
address += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instruction.mnemonic == ZYDIS_MNEMONIC_PUSH && !instructions.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto rva = static_cast<uint32_t>(address - BaseAddress);
|
||||
instructions.emplace_back(rva, instruction, operands, instruction.operand_count_visible);
|
||||
|
||||
address += instruction.length;
|
||||
|
||||
if (maxInstructions != -1 && instructions.size() >= maxInstructions)
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get the count of data references in the instructions (only second oprand of mov)
|
||||
/// </summary>
|
||||
/// <param name="instructions"></param>
|
||||
/// <returns></returns>
|
||||
int32_t GetDataReferenceCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
|
||||
if (instr.Instruction.mnemonic != ZYDIS_MNEMONIC_MOV)
|
||||
return false;
|
||||
|
||||
if (instr.Operands.size() != 2)
|
||||
return false;
|
||||
|
||||
const auto& op = instr.Operands[1];
|
||||
|
||||
// access to memory, based off of rip, 32-bit displacement
|
||||
return op.type == ZYDIS_OPERAND_TYPE_MEMORY && op.mem.base == ZYDIS_REGISTER_RIP && op.mem.disp.has_displacement;
|
||||
}));
|
||||
}
|
||||
|
||||
int32_t GetCallCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
|
||||
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL;
|
||||
}));
|
||||
}
|
||||
|
||||
int32_t GetUniqueCallCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
std::unordered_set<uint32_t> calls;
|
||||
for (const auto& instr : instructions)
|
||||
{
|
||||
if (instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL) {
|
||||
uint32_t destination = instr.Operands[0].imm.value.s + instr.RVA + instr.Instruction.length;
|
||||
calls.insert(destination);
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<int32_t>(calls.size());
|
||||
}
|
||||
|
||||
int32_t GetCmpImmCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
|
||||
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP && instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && instr.Operands[1].imm.value.u;
|
||||
}));
|
||||
}
|
||||
|
||||
void ResolveAchivementCmdId()
|
||||
{
|
||||
if (Globals::AchievementId != 0)
|
||||
return;
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
|
||||
std::println("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
|
||||
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
|
||||
|
||||
if (il2cppSection.empty())
|
||||
return; // message box?
|
||||
|
||||
const auto candidates = Util::PatternScanAll(il2cppSection, "56 48 83 EC 20 48 89 D0 48 89 CE 80 3D ? ? ? ? 00");
|
||||
std::println("Candidates: {}", candidates.size());
|
||||
|
||||
std::vector<std::vector<DecodedInstruction>> filteredInstructions;
|
||||
std::ranges::copy_if(
|
||||
candidates | std::views::transform([](auto va) { return DecodeFunction(va); }),
|
||||
std::back_inserter(filteredInstructions),
|
||||
[](const std::vector<DecodedInstruction>& instr) {
|
||||
return GetDataReferenceCount(instr) == 5 && GetCallCount(instr) == 10 &&
|
||||
GetUniqueCallCount(instr) == 6 && GetCmpImmCount(instr) == 5;
|
||||
});
|
||||
|
||||
// should have only one result
|
||||
if (filteredInstructions.size() != 1)
|
||||
{
|
||||
std::println("Filtered Instructions: {}", filteredInstructions.size());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& instructions = filteredInstructions[0];
|
||||
std::println("RVA: 0x{:08X}", instructions.front().RVA);
|
||||
|
||||
// extract all the non-zero immediate values from the cmp instructions
|
||||
std::vector<uint32_t> cmdIds;
|
||||
std::ranges::for_each(instructions, [&cmdIds](const DecodedInstruction& instr) {
|
||||
if (instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP &&
|
||||
instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
|
||||
instr.Operands[1].imm.value.u != 0) {
|
||||
cmdIds.push_back(static_cast<uint32_t>(instr.Operands[1].imm.value.u));
|
||||
}
|
||||
});
|
||||
|
||||
for (const auto& cmdId : cmdIds)
|
||||
{
|
||||
std::println("AchievementId: {}", cmdId);
|
||||
Globals::AchievementIdSet.insert(static_cast<uint16_t>(cmdId));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
std::vector<uintptr_t> GetCalls(uint8_t* target)
|
||||
{
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
const auto sectionAddress = reinterpret_cast<uintptr_t>(il2cppSection.data());
|
||||
const auto sectionSize = il2cppSection.size();
|
||||
|
||||
std::vector<uintptr_t> callSites;
|
||||
const __m128i callOpcode = _mm_set1_epi8(0xE8);
|
||||
const size_t simdEnd = sectionSize / 16 * 16;
|
||||
|
||||
for (size_t i = 0; i < simdEnd; i += 16) {
|
||||
// load 16 bytes from the current address
|
||||
const __m128i chunk = _mm_loadu_si128((__m128i*)(sectionAddress + i));
|
||||
|
||||
// compare the loaded chunk with 0xE8 in all 16 bytes
|
||||
const __m128i result = _mm_cmpeq_epi8(chunk, callOpcode);
|
||||
|
||||
// move the comparison results into a mask
|
||||
int mask = _mm_movemask_epi8(result);
|
||||
|
||||
while (mask != 0) {
|
||||
DWORD first_match_idx = 0;
|
||||
_BitScanForward(&first_match_idx, mask); // index of the first set bit (match)
|
||||
|
||||
// index of the instruction
|
||||
const size_t instruction_index = i + first_match_idx;
|
||||
|
||||
const int32_t delta = *(int32_t*)(sectionAddress + instruction_index + 1);
|
||||
const uintptr_t dest = sectionAddress + instruction_index + 5 + delta;
|
||||
|
||||
if (dest == (uintptr_t)target) {
|
||||
callSites.push_back(sectionAddress + instruction_index);
|
||||
}
|
||||
|
||||
// clear the bit we just processed and continue with the next match
|
||||
mask &= ~(1 << first_match_idx);
|
||||
}
|
||||
}
|
||||
|
||||
return callSites;
|
||||
}
|
||||
|
||||
uintptr_t FindFunctionEntry(uintptr_t address) // not a correct way to find function entry
|
||||
{
|
||||
__try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// go back to 'sub rsp' instruction
|
||||
uint32_t code = *(uint32_t*)address;
|
||||
code &= ~0xFF000000;
|
||||
|
||||
if (_byteswap_ulong(code) == 0x4883EC00) { // sub rsp, ??
|
||||
return address;
|
||||
}
|
||||
|
||||
address--;
|
||||
}
|
||||
|
||||
}
|
||||
__except (1) {}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
void Resolve_BitConverter_ToUInt16()
|
||||
{
|
||||
if (Globals::Offset.BitConverter_ToUInt16 != 0) {
|
||||
Globals::Offset.BitConverter_ToUInt16 += Globals::BaseAddress;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
|
||||
std::print("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
|
||||
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
|
||||
|
||||
/*
|
||||
mov ecx, 0Fh
|
||||
call ThrowHelper.ThrowArgumentNullException
|
||||
mov ecx, 0Eh
|
||||
mov edx, 16h
|
||||
call ThrowHelper.ThrowArgumentOutOfRangeException
|
||||
mov ecx, 5
|
||||
call ThrowHelper.ThrowArgumentException
|
||||
*/
|
||||
auto candidates = Util::PatternScanAll(il2cppSection, "B9 0F 00 00 00 E8 ? ? ? ? B9 0E 00 00 00 BA 16 00 00 00 E8 ? ? ? ? B9 05 00 00 00 E8 ? ? ? ?");
|
||||
std::println("Candidates: {}", candidates.size());
|
||||
|
||||
std::vector<uintptr_t> filteredEntries;
|
||||
std::ranges::copy_if(candidates, std::back_inserter(filteredEntries), [](uintptr_t& entry) {
|
||||
entry = FindFunctionEntry(entry);
|
||||
return entry % 16 == 0;
|
||||
});
|
||||
|
||||
for (const auto& entry : filteredEntries)
|
||||
{
|
||||
std::println("Entry: 0x{:X}", entry);
|
||||
}
|
||||
|
||||
std::println("Looking for call counts...");
|
||||
std::mutex mutex;
|
||||
std::unordered_map<uintptr_t, int32_t> callCounts;
|
||||
// find the call counts to candidate functions
|
||||
std::vector<std::future<void>> futures;
|
||||
std::ranges::transform(filteredEntries, std::back_inserter(futures), [&](uintptr_t entry) {
|
||||
return std::async(std::launch::async, [&](uintptr_t e) {
|
||||
const auto callSites = GetCalls((uint8_t*)e);
|
||||
std::lock_guard lock(mutex);
|
||||
callCounts[e] = callSites.size();
|
||||
}, entry);
|
||||
});
|
||||
|
||||
for (auto& future : futures) {
|
||||
future.get();
|
||||
}
|
||||
|
||||
uintptr_t targetEntry = 0;
|
||||
for (const auto& [entry, count] : callCounts)
|
||||
{
|
||||
std::println("Entry: 0x{:X}, RVA: 0x{:08X}, Count: {}", entry, entry - Globals::BaseAddress, count);
|
||||
if (count == 3) {
|
||||
targetEntry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
Globals::Offset.BitConverter_ToUInt16 = targetEntry;
|
||||
}
|
||||
|
||||
void ResolveInventoryCmdId()
|
||||
{
|
||||
if (Globals::PlayerStoreId != 0)
|
||||
return;
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
std::println("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
|
||||
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
|
||||
|
||||
/*
|
||||
cmp r8d, 2
|
||||
jz 0x3B
|
||||
cmd r8d, 1
|
||||
mov rax
|
||||
*/
|
||||
|
||||
// look for ItemModule.GetBagManagerByStoreType <- mf got inlined in 5.5
|
||||
// we just gon to look for OnPlayerStoreNotify
|
||||
const auto candidates = Util::PatternScanAll(il2cppSection, "41 83 F8 02 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 41 83 F8 01");
|
||||
std::println("Candidates: {}", candidates.size());
|
||||
if (candidates.empty())
|
||||
return;
|
||||
|
||||
uintptr_t pOnPlayerStoreNotify = 0;
|
||||
{
|
||||
// one of the candidates is OnPlayerStoreNotify
|
||||
// search after the pattern to find an arbirary branch
|
||||
auto decodedInstructions = candidates | std::views::transform([](auto va) { return DecodeFunction(va, 20); });
|
||||
|
||||
// find the call site with an arbitrary branch (JMP or CALL) after the call
|
||||
auto targetInstructions = std::ranges::find_if(decodedInstructions, [](const auto& instr) {
|
||||
return std::ranges::any_of(instr, [](const DecodedInstruction& i) {
|
||||
return (i.Instruction.mnemonic == ZYDIS_MNEMONIC_JMP || i.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL) &&
|
||||
i.Operands.size() == 1 && i.Operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER;
|
||||
});
|
||||
});
|
||||
|
||||
if (targetInstructions == decodedInstructions.end()) {
|
||||
std::println("Failed to find target instruction");
|
||||
return;
|
||||
}
|
||||
|
||||
// ItemModule.OnPlayerStoreNotify
|
||||
const auto& instructions = *targetInstructions;
|
||||
pOnPlayerStoreNotify = Globals::BaseAddress + instructions.front().RVA;
|
||||
|
||||
const auto isFunctionEntry = [](uintptr_t va) -> bool {
|
||||
auto* code = reinterpret_cast<uint8_t*>(va);
|
||||
return (va % 16 == 0 &&
|
||||
code[0] == 0x56 && // push rsi
|
||||
(*reinterpret_cast<uint32_t*>(&code[1]) & ~0xFF000000) == _byteswap_ulong(0x4883EC00)); // sub rsp, ??
|
||||
};
|
||||
|
||||
auto range = std::views::iota(0, 126);
|
||||
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(pOnPlayerStoreNotify - i); });
|
||||
it != range.end())
|
||||
{
|
||||
pOnPlayerStoreNotify -= *it;
|
||||
}
|
||||
else {
|
||||
std::println("Failed to find function entry");
|
||||
return;
|
||||
}
|
||||
|
||||
std::println("OnPlayerStoreNotify: 0x{:X}", pOnPlayerStoreNotify);
|
||||
}
|
||||
|
||||
uintptr_t pOnPacket = 0;
|
||||
{
|
||||
// get all calls to OnPlayerStoreNotify
|
||||
const auto calls = GetCalls(reinterpret_cast<uint8_t*>(pOnPlayerStoreNotify));
|
||||
if (calls.size() != 1) {
|
||||
std::println("Failed to find call site");
|
||||
return;
|
||||
}
|
||||
|
||||
// ItemModule.OnPacket - search backwards for function entry
|
||||
pOnPacket = calls.front();
|
||||
const auto isFunctionEntry = [](uintptr_t va) -> bool {
|
||||
auto* code = reinterpret_cast<uint8_t*>(va);
|
||||
return (va % 16 == 0 &&
|
||||
code[0] == 0x56 && // push rsi
|
||||
(*reinterpret_cast<uint32_t*>(&code[1]) & ~0xFF000000) == _byteswap_ulong(0x4883EC00)); // sub rsp, ??
|
||||
};
|
||||
|
||||
auto range = std::views::iota(0, 3044);
|
||||
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(pOnPacket - i); });
|
||||
it != range.end())
|
||||
{
|
||||
pOnPacket -= *it;
|
||||
}
|
||||
else {
|
||||
std::println("Failed to find function entry");
|
||||
return;
|
||||
}
|
||||
|
||||
std::println("OnPacket: 0x{:X}", pOnPacket);
|
||||
}
|
||||
|
||||
const auto decodedInstructions = DecodeFunction(pOnPacket);
|
||||
uint32_t cmdid = 0;
|
||||
std::ranges::for_each(decodedInstructions, [&cmdid, pOnPlayerStoreNotify](const DecodedInstruction& i) {
|
||||
static uint32_t immValue = 0; // keep track of the last immediate value
|
||||
|
||||
if (i.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP &&
|
||||
i.Operands.size() == 2 &&
|
||||
i.Operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
||||
i.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE)
|
||||
{
|
||||
immValue = static_cast<uint32_t>(i.Operands[1].imm.value.u);
|
||||
}
|
||||
|
||||
if (i.Instruction.meta.branch_type == ZYDIS_BRANCH_TYPE_NEAR && i.Operands.size() == 1 &&
|
||||
(i.Instruction.mnemonic == ZYDIS_MNEMONIC_JZ || i.Instruction.mnemonic == ZYDIS_MNEMONIC_JNZ)) // jz for true branch, jnz for false branch
|
||||
{
|
||||
// assume the branching is jz
|
||||
uintptr_t branchAddr = Globals::BaseAddress + i.RVA + i.Instruction.length + i.Operands[0].imm.value.s;
|
||||
|
||||
// check if the branch is jnz and adjust the branch address
|
||||
if (i.Instruction.mnemonic == ZYDIS_MNEMONIC_JNZ) {
|
||||
branchAddr = Globals::BaseAddress + i.RVA + i.Instruction.length;
|
||||
}
|
||||
|
||||
// decode the branch address immediately
|
||||
const auto instructions = DecodeFunction(branchAddr, 10);
|
||||
const auto isMatch = std::ranges::any_of(instructions, [pOnPlayerStoreNotify](const DecodedInstruction& instr) {
|
||||
if (instr.Instruction.mnemonic != ZYDIS_MNEMONIC_CALL)
|
||||
return false;
|
||||
|
||||
uintptr_t destination = 0;
|
||||
ZydisCalcAbsoluteAddress(&instr.Instruction, instr.Operands.data(), Globals::BaseAddress + instr.RVA, &destination);
|
||||
return destination == pOnPlayerStoreNotify;
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
cmdid = immValue;
|
||||
}
|
||||
|
||||
}
|
||||
return cmdid == 0; // stop processing if cmdid is found
|
||||
});
|
||||
|
||||
Globals::PlayerStoreId = static_cast<uint16_t>(cmdid);
|
||||
std::println("PlayerStoreId: {}", Globals::PlayerStoreId);
|
||||
}
|
||||
|
||||
void Resolve_AccountDataItem_UpdateNormalProp()
|
||||
{
|
||||
if (Globals::Offset.AccountDataItem_UpdateNormalProp != 0) {
|
||||
Globals::Offset.AccountDataItem_UpdateNormalProp += Globals::BaseAddress;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
|
||||
/*
|
||||
add ??, 0FFFFD8EEh
|
||||
cmp ??, 30h
|
||||
*/
|
||||
auto candidates = Util::PatternScanAll(il2cppSection, "81 ? EE D8 FF FF ? 83 ? 30");
|
||||
// should have only one result
|
||||
if (candidates.size() != 1)
|
||||
{
|
||||
std::println("Filtered Instructions: {}", candidates.size());
|
||||
return;
|
||||
}
|
||||
auto fp = candidates[0];
|
||||
|
||||
const auto isFunctionEntry = [](uintptr_t va) -> bool {
|
||||
auto* code = reinterpret_cast<uint8_t*>(va);
|
||||
/* push rsi */
|
||||
/* push rdi */
|
||||
return (va % 16 == 0 && code[0] == 0x56 && code[1] == 0x57);
|
||||
};
|
||||
|
||||
auto range = std::views::iota(0, 213);
|
||||
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(fp - i); }); it != range.end()) {
|
||||
fp -= *it;
|
||||
} else {
|
||||
std::println("Failed to find function entry");
|
||||
return;
|
||||
}
|
||||
|
||||
Globals::Offset.AccountDataItem_UpdateNormalProp = fp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool InitIL2CPP()
|
||||
{
|
||||
std::string buffer;
|
||||
buffer.resize(MAX_PATH);
|
||||
ZeroMemory(buffer.data(), MAX_PATH);
|
||||
const auto pathLength = GetModuleFileNameA(nullptr, buffer.data(), MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
buffer.resize(pathLength);
|
||||
ZeroMemory(buffer.data(), pathLength);
|
||||
GetModuleFileNameA(nullptr, buffer.data(), pathLength);
|
||||
}
|
||||
buffer.shrink_to_fit();
|
||||
|
||||
using namespace Globals;
|
||||
IsCNREL = buffer.find("YuanShen.exe") != std::string::npos;
|
||||
BaseAddress = (uintptr_t)GetModuleHandleA(nullptr);
|
||||
|
||||
std::future<void> resolveFuncFuture = std::async(std::launch::async, Resolve_BitConverter_ToUInt16);
|
||||
std::future<void> resolveCmdIdFuture = std::async(std::launch::async, ResolveAchivementCmdId);
|
||||
std::future<void> resolveInventoryFuture = std::async(std::launch::async, ResolveInventoryCmdId);
|
||||
std::future<void> resolveUpdatePropFuture = std::async(std::launch::async, Resolve_AccountDataItem_UpdateNormalProp);
|
||||
|
||||
resolveFuncFuture.get();
|
||||
resolveCmdIdFuture.get();
|
||||
resolveInventoryFuture.get();
|
||||
resolveUpdatePropFuture.get();
|
||||
|
||||
std::println("BaseAddress: 0x{:X}", BaseAddress);
|
||||
std::println("IsCNREL: {:d}", IsCNREL);
|
||||
std::println("BitConverter_ToUInt16: 0x{:X}", Offset.BitConverter_ToUInt16);
|
||||
std::println("AccountDataItem_UpdateNormalProp: 0x{:X}", Offset.AccountDataItem_UpdateNormalProp);
|
||||
|
||||
if (!AchievementId && AchievementIdSet.empty())
|
||||
{
|
||||
Util::ErrorDialog("Failed to resolve achievement data");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PlayerStoreId)
|
||||
{
|
||||
Util::ErrorDialog("Failed to resolve inventory data");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
4
lib/src/il2cpp-init.h
Normal file
4
lib/src/il2cpp-init.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
// IL2CPP application initializer
|
||||
bool InitIL2CPP();
|
||||
78
lib/src/il2cpp-types.h
Normal file
78
lib/src/il2cpp-types.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
#define PROPERTY_GET_CONST(type, name, funcBody) \
|
||||
type get_##name() const funcBody \
|
||||
__declspec(property(get = get_##name)) type name;
|
||||
|
||||
enum class PacketType : uint8_t
|
||||
{
|
||||
None = 0,
|
||||
Achivement = 1,
|
||||
Inventory = 2,
|
||||
PropData = 100,
|
||||
End = 255,
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class Array
|
||||
{
|
||||
public:
|
||||
void* klass;
|
||||
void* monitor;
|
||||
void* bounds;
|
||||
size_t max_length;
|
||||
T vector[1];
|
||||
|
||||
Array() = delete;
|
||||
|
||||
T* data() {
|
||||
return vector;
|
||||
}
|
||||
|
||||
std::span<T> AsSpan() {
|
||||
return { vector, max_length };
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
U As() {
|
||||
return reinterpret_cast<U>(vector);
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(alignof(Array<uint8_t>) == 8, "Array alignment is incorrect");
|
||||
static_assert(offsetof(Array<uint8_t>, vector) == 32, "vector offset is incorrect");
|
||||
|
||||
#pragma pack(push, 1)
|
||||
class PacketMeta
|
||||
{
|
||||
uint16_t m_HeadMagic;
|
||||
uint16_t m_CmdId;
|
||||
uint16_t m_HeaderLength;
|
||||
uint32_t m_DataLength;
|
||||
uint8_t m_Data[1];
|
||||
public:
|
||||
|
||||
PacketMeta() = delete;
|
||||
|
||||
PROPERTY_GET_CONST(uint16_t, HeadMagic, { return _byteswap_ushort(m_HeadMagic); });
|
||||
PROPERTY_GET_CONST(uint16_t, CmdId, { return _byteswap_ushort(m_CmdId); });
|
||||
PROPERTY_GET_CONST(uint16_t, HeaderLength, { return _byteswap_ushort(m_HeaderLength); });
|
||||
PROPERTY_GET_CONST(uint32_t, DataLength, { return _byteswap_ulong(m_DataLength); });
|
||||
|
||||
std::span<uint8_t> AsSpan() {
|
||||
return { m_Data + HeaderLength, DataLength };
|
||||
}
|
||||
|
||||
friend struct PacketMetaStaticAssertHelper;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct PacketMetaStaticAssertHelper
|
||||
{
|
||||
static_assert(offsetof(PacketMeta, m_CmdId) == 2, "CmdId offset is incorrect");
|
||||
static_assert(offsetof(PacketMeta, m_HeaderLength) == 4, "HeadLength offset is incorrect");
|
||||
static_assert(offsetof(PacketMeta, m_DataLength) == 6, "DataLength offset is incorrect");
|
||||
static_assert(offsetof(PacketMeta, m_Data) == 10, "Data offset is incorrect");
|
||||
};
|
||||
9
lib/src/ntprivate.h
Normal file
9
lib/src/ntprivate.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <bcrypt.h>
|
||||
|
||||
#pragma comment(lib, "ntdll.lib")
|
||||
|
||||
#define LDR_ADDREF_DLL_PIN 0x00000001
|
||||
|
||||
EXTERN_C NTSYSAPI NTSTATUS NTAPI LdrAddRefDll(_In_ ULONG Flags, _In_ PVOID DllHandle);
|
||||
210
lib/src/util.cpp
Normal file
210
lib/src/util.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <ranges>
|
||||
#include <intrin.h>
|
||||
#include "util.h"
|
||||
|
||||
#include "globals.h"
|
||||
|
||||
#ifdef _DEBUG
|
||||
#pragma runtime_checks("", off)
|
||||
#endif
|
||||
|
||||
#pragma region FindMainWindowByPID
|
||||
|
||||
namespace
|
||||
{
|
||||
struct HandleData {
|
||||
DWORD Pid;
|
||||
HWND Hwnd;
|
||||
};
|
||||
|
||||
bool IsMainWindow(HWND handle) {
|
||||
return GetWindow(handle, GW_OWNER) == nullptr && IsWindowVisible(handle) == TRUE;
|
||||
}
|
||||
|
||||
bool IsUnityWindow(HWND handle) {
|
||||
char szName[256]{};
|
||||
GetClassNameA(handle, szName, 256);
|
||||
return _stricmp(szName, "UnityWndClass") == 0;
|
||||
}
|
||||
|
||||
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
|
||||
HandleData& data = *(HandleData*)lParam;
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(handle, &pid);
|
||||
if (data.Pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle))
|
||||
return TRUE;
|
||||
data.Hwnd = handle;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
std::tuple<std::vector<uint8_t>, std::string> PatternToBytes(const char* pattern)
|
||||
{
|
||||
std::vector<uint8_t> bytes;
|
||||
std::string mask;
|
||||
|
||||
const auto start = const_cast<char*>(pattern);
|
||||
const auto end = const_cast<char*>(pattern) + strlen(pattern);
|
||||
|
||||
for (auto current = start; current < end; ++current) {
|
||||
if (*current == '?') {
|
||||
++current;
|
||||
if (*current == '?')
|
||||
++current;
|
||||
bytes.push_back(-1);
|
||||
mask.push_back('?');
|
||||
}
|
||||
else {
|
||||
bytes.push_back(strtoul(current, ¤t, 16));
|
||||
mask.push_back('x');
|
||||
}
|
||||
}
|
||||
return { bytes, mask };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
static constexpr LPCSTR base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
namespace Util
|
||||
{
|
||||
HWND FindMainWindowByPID(DWORD pid)
|
||||
{
|
||||
HandleData data = {
|
||||
.Pid = pid,
|
||||
.Hwnd = nullptr
|
||||
};
|
||||
EnumWindows(EnumWindowsCallback, (LPARAM)&data);
|
||||
return data.Hwnd;
|
||||
}
|
||||
|
||||
std::string Base64Encode(std::span<uint8_t> data)
|
||||
{
|
||||
return Base64Encode(data.data(), data.size());
|
||||
}
|
||||
|
||||
std::string Base64Encode(uint8_t const* buf, size_t bufLen)
|
||||
{
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
uint8_t char_array_3[3];
|
||||
uint8_t char_array_4[4];
|
||||
while (bufLen--) {
|
||||
char_array_3[i++] = *buf++;
|
||||
if (i == 3) {
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
for (i = 0; (i < 4); i++)
|
||||
ret += base64_chars[char_array_4[i]];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
if (i) {
|
||||
int j;
|
||||
for (j = i; j < 3; j++)
|
||||
char_array_3[j] = '\0';
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
for (j = 0; j < i + 1; j++)
|
||||
ret += base64_chars[char_array_4[j]];
|
||||
while (i++ < 3)
|
||||
ret += '=';
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ErrorDialog(LPCSTR title, LPCSTR msg)
|
||||
{
|
||||
MessageBoxA(Globals::GameWindow, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL);
|
||||
}
|
||||
|
||||
void ErrorDialog(LPCSTR msg)
|
||||
{
|
||||
ErrorDialog("YaeAchievement", msg);
|
||||
}
|
||||
|
||||
void Win32ErrorDialog(DWORD code, DWORD winerrcode)
|
||||
{
|
||||
const std::string msg = "CRITICAL ERROR!\nError code: " + std::to_string(winerrcode) + "-" + std::to_string(code) +
|
||||
"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!";
|
||||
|
||||
ErrorDialog("YaeAchievement", msg.c_str());
|
||||
}
|
||||
|
||||
std::vector<uintptr_t> PatternScanAll(std::span<uint8_t> bytes, const char* pattern)
|
||||
{
|
||||
std::vector<uintptr_t> results;
|
||||
const auto [patternBytes, patternMask] = PatternToBytes(pattern);
|
||||
constexpr std::size_t chunkSize = 16;
|
||||
|
||||
const auto maskCount = static_cast<std::size_t>(std::ceil(patternMask.size() / chunkSize));
|
||||
std::array<int32_t, 32> masks{};
|
||||
|
||||
auto chunks = patternMask | std::views::chunk(chunkSize);
|
||||
for (std::size_t i = 0; auto chunk : chunks) {
|
||||
int32_t mask = 0;
|
||||
for (std::size_t j = 0; j < chunk.size(); ++j) {
|
||||
if (chunk[j] == 'x') {
|
||||
mask |= 1 << j;
|
||||
}
|
||||
}
|
||||
masks[i++] = mask;
|
||||
}
|
||||
|
||||
__m128i xmm1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(patternBytes.data()));
|
||||
__m128i xmm2, xmm3, mask;
|
||||
|
||||
auto pData = bytes.data();
|
||||
const auto end = pData + bytes.size() - patternMask.size();
|
||||
|
||||
while (pData < end)
|
||||
{
|
||||
_mm_prefetch(reinterpret_cast<const char*>(pData + 64), _MM_HINT_NTA);
|
||||
|
||||
if (patternBytes[0] == pData[0])
|
||||
{
|
||||
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pData));
|
||||
mask = _mm_cmpeq_epi8(xmm1, xmm2);
|
||||
|
||||
if ((_mm_movemask_epi8(mask) & masks[0]) == masks[0])
|
||||
{
|
||||
bool found = true;
|
||||
for (int i = 1; i < maskCount; ++i)
|
||||
{
|
||||
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pData + i * chunkSize));
|
||||
xmm3 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(patternBytes.data() + i * chunkSize));
|
||||
mask = _mm_cmpeq_epi8(xmm2, xmm3);
|
||||
if ((_mm_movemask_epi8(mask) & masks[i]) != masks[i])
|
||||
{
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (found) {
|
||||
results.push_back(reinterpret_cast<uintptr_t>(pData));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
++pData;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
#pragma runtime_checks("", restore)
|
||||
#endif
|
||||
19
lib/src/util.h
Normal file
19
lib/src/util.h
Normal file
@@ -0,0 +1,19 @@
|
||||
// ReSharper disable CppClangTidyClangDiagnosticLanguageExtensionToken
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace Util
|
||||
{
|
||||
HWND FindMainWindowByPID(DWORD pid);
|
||||
std::string Base64Encode(std::span<uint8_t> data);
|
||||
std::string Base64Encode(uint8_t const* buf, size_t bufLen);
|
||||
|
||||
void ErrorDialog(LPCSTR title, LPCSTR msg);
|
||||
void ErrorDialog(LPCSTR msg);
|
||||
void Win32ErrorDialog(DWORD code, DWORD winerrcode);
|
||||
|
||||
std::vector<uintptr_t> PatternScanAll(std::span<uint8_t> bytes, const char* pattern);
|
||||
}
|
||||
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,76 +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 {
|
||||
bytes field0 = 11;
|
||||
bytes field1 = 12;
|
||||
bytes field2 = 13;
|
||||
msg0 info = 3;
|
||||
}
|
||||
|
||||
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;
|
||||
uint32 field6 = 14;
|
||||
string field7 = 16;
|
||||
uint32 field8 = 18;
|
||||
string field9 = 19;
|
||||
string fieldA = 20;
|
||||
bytes fieldB = 23;
|
||||
string fieldC = 24;
|
||||
string fieldD = 26;
|
||||
string fieldE = 27;
|
||||
string fieldF = 30;
|
||||
string fieldG = 31;
|
||||
string fieldH = 32;
|
||||
string fieldI = 33;
|
||||
msg1 fieldJ = 22;
|
||||
}
|
||||
|
||||
message msg1 {
|
||||
uint32 field0 = 1;
|
||||
string field1 = 3;
|
||||
string field2 = 4;
|
||||
string field3 = 5;
|
||||
string field4 = 6;
|
||||
}
|
||||
|
||||
message QueryRegionList {
|
||||
bytes field0 = 5;
|
||||
bytes field1 = 6;
|
||||
bool field2 = 7;
|
||||
repeated msg2 list = 2;
|
||||
}
|
||||
|
||||
message msg2 {
|
||||
string field0 = 1;
|
||||
string field1 = 2;
|
||||
string field2 = 3;
|
||||
string url = 4;
|
||||
}
|
||||
100
regionServer.js
100
regionServer.js
@@ -1,100 +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
|
||||
}
|
||||
})
|
||||
const regions = await decodeProto(Buffer.from(d.data,"base64"),"QueryRegionList")
|
||||
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"
|
||||
})
|
||||
if (noQueryRequest) {
|
||||
preparedRegions[host] = true
|
||||
return d.data
|
||||
} else {
|
||||
const region = await decodeProto(Buffer.from(d.data,"base64"),"QueryCurRegion")
|
||||
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
|
||||
})
|
||||
response.end(frontResponse.data)
|
||||
}
|
||||
})
|
||||
hServer.on("error", err => {
|
||||
if (err["code"] === "EADDRINUSE") {
|
||||
console.log("本机 443 端口被其它程序占用,请关闭后重试")
|
||||
}
|
||||
throw err
|
||||
})
|
||||
hServer.listen(443, "127.0.0.1", () => {
|
||||
regionListLoadedCallback()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
293
utils.js
293
utils.js
@@ -1,293 +0,0 @@
|
||||
const fs = require("fs")
|
||||
const dns = require("dns")
|
||||
const ini = require("ini")
|
||||
const util = require("util")
|
||||
const zlib = require("zlib")
|
||||
const cloud = require("./secret")
|
||||
const readline = require("readline")
|
||||
const protobuf = require("protobufjs")
|
||||
const { version } = require("./version")
|
||||
const { createHash } = require("crypto")
|
||||
const path = require("path")
|
||||
const messages = path.join(__dirname, "./proto/Messages.proto")
|
||||
let axios = require("axios")
|
||||
|
||||
const sleep = ms => new Promise(resolve => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
|
||||
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, cb) => {
|
||||
if (!fs.existsSync(`${path}/UnityPlayer.dll`) && !fs.existsSync(`${path}/pkg_version`)) {
|
||||
throw Error(`路径有误: ${path}`)
|
||||
} else {
|
||||
cb(path)
|
||||
}
|
||||
}
|
||||
|
||||
let conf
|
||||
|
||||
const initConfig = async () => {
|
||||
const configFileName = "./config.json"
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
const question = (query) => new Promise(resolve => {
|
||||
rl.question(query, resolve)
|
||||
})
|
||||
const lookup = util.promisify(dns.lookup).bind(dns)
|
||||
if (fs.existsSync(configFileName)) {
|
||||
conf = JSON.parse(fs.readFileSync(configFileName, "utf-8"))
|
||||
} else {
|
||||
const p = await question("原神主程序(YuanShen.exe或GenshinImpact.exe)所在路径: (支持多个路径, 使用符号'*'分隔)\n")
|
||||
conf = {
|
||||
path: [],
|
||||
offlineResource: false,
|
||||
customCDN: ""
|
||||
}
|
||||
p.split("*").forEach(s => {
|
||||
checkPath(s, () => {
|
||||
if (!conf.path.includes(s)) {
|
||||
conf.path.push(s)
|
||||
}
|
||||
})
|
||||
})
|
||||
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
|
||||
rl.close()
|
||||
}
|
||||
if (conf.proxy !== undefined) {
|
||||
axios = axios.create({
|
||||
proxy: conf.proxy
|
||||
})
|
||||
}
|
||||
if (conf.path.length === 1) {
|
||||
checkPath(conf.path[0], p => {
|
||||
conf.path = p
|
||||
})
|
||||
} else {
|
||||
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> `)
|
||||
checkPath(conf.path[parseInt(idx)], 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)
|
||||
// noinspection JSUnresolvedVariable
|
||||
const genshinConf = ini.parse(fs.readFileSync(conf.path + "/config.ini", "utf-8")).General
|
||||
conf.channel = genshinConf.channel
|
||||
// noinspection JSUnresolvedVariable
|
||||
conf.subChannel = genshinConf.sub_channel
|
||||
conf.version = readGameRes("/Persistent/ChannelName").toString() + readGameRes("/Persistent/ScriptVersion").toString()
|
||||
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 lookup(conf.dispatchUrl, 4)).address
|
||||
return conf
|
||||
}
|
||||
|
||||
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 "没有可用的CDN"
|
||||
}
|
||||
|
||||
const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
|
||||
console.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) {
|
||||
console.log("文件 %s 命中缓存", fp)
|
||||
const etagLength = fd.readUInt8()
|
||||
return JSON.parse(fd.subarray(1 + etagLength).toString())
|
||||
} else {
|
||||
console.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)
|
||||
console.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) {
|
||||
console.log(`有可用更新: ${version.name} => ${data["vn"]}`)
|
||||
console.log(`更新内容: \n${data["ds"]}`)
|
||||
console.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 = ""
|
||||
|
||||
const setupHost = (restore = false) => {
|
||||
const path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
|
||||
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, sleep, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
|
||||
setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
const version = {
|
||||
code: 2,
|
||||
name: "1.0.1"
|
||||
}
|
||||
|
||||
module.exports = { version }
|
||||
Reference in New Issue
Block a user