260 Commits
1.1 ... 3.7.0

Author SHA1 Message Date
HolographicHat
f737122247 4.7 2024-06-06 02:07:56 +08:00
HolographicHat
520167ef85 fix #82 #101 2024-04-29 14:38:13 +08:00
HolographicHat
faee6f6121 Update README_EN.md 2024-04-25 21:50:15 +08:00
HolographicHat
06c5468118 update readme 2024-04-25 21:42:14 +08:00
HolographicHat
b7c2204f68 Update CI 2024-04-25 02:52:46 +08:00
HolographicHat
5dc5e646d6 fix #87 2024-04-25 02:45:30 +08:00
HolographicHat
9cab7e8702 4.6 2024-04-24 23:26:24 +08:00
HolographicHat
1f080fe084 Merge pull request #98 from Anong0u0/patch 2024-04-22 14:27:29 +08:00
Anong0u0
c8497243c0 Fix GamePathRegex match to case insensitive 2024-04-22 13:44:20 +08:00
HolographicHat
9abdd123ee #93
remove appcenter
2024-04-02 15:06:05 +08:00
HolographicHat
e1429289ad refactor appcenter 2024-03-15 00:08:34 +08:00
HolographicHat
1f311ed987 4.5 2024-03-14 23:55:36 +08:00
HolographicHat
cc346915e3 Merge pull request #85 from BTMuli/master
️ 延长刷新间隔,Y/YES均可使用旧数据(大小写不敏感)
2024-02-06 23:27:16 +08:00
HolographicHat
cd0f49d83d shorten code 2024-02-06 23:19:41 +08:00
目棃
d0b7d15894 Merge branch 'master' into master 2024-02-06 22:54:51 +08:00
目棃
504c8a2a9a 👔 默认不使用旧数据 2024-02-06 22:46:37 +08:00
HolographicHat
b3162052da refactor 2024-02-06 21:31:14 +08:00
目棃
034d999d25 ️ 延长刷新间隔,回车/Y也能使用旧数据了 2024-02-02 19:30:03 +08:00
HolographicHat
45d5620e83 4.4 2024-01-31 12:46:31 +08:00
HolographicHat
fa13f9c8e5 fix 2023-12-25 00:21:48 +08:00
HolographicHat
2210a97d61 4.3 2023-12-24 23:59:24 +08:00
HolographicHat
feb7ac44da 4.2 2023-11-09 21:39:05 +08:00
HolographicHat
3924129560 Merge pull request #74 from Lightczx/master
Disable marshalling
2023-10-30 14:04:24 +08:00
Lightczx
4f7f0cdfd2 disable marshalling 2023-10-30 13:51:01 +08:00
HolographicHat
cf0753c676 Merge pull request #73 from BTMuli/master
🐛 修复 TeyvatGuide 链接错误
2023-10-16 13:42:22 +08:00
BTMuli
0b895d47ca 🐛 修复 TeyvatGuide 链接错误 2023-10-16 12:39:32 +08:00
HolographicHat
78d2722e20 Merge pull request #72 from BTMuli/master
更新文档
2023-10-15 01:16:19 +08:00
BTMuli
385c673323 🚨 revert,删除多余空格 2023-10-15 00:31:18 +08:00
BTMuli
50beb2cce7 🚨 修复 CI 报错 2023-10-15 00:18:39 +08:00
BTMuli
324a4153e0 📝 删除混入其中的表格文件 2023-10-14 23:56:24 +08:00
BTMuli
3de459aceb 📝 更新部分信息 2023-10-14 23:54:49 +08:00
BTMuli
295bb89177 📝 更新导出说明 2023-10-14 23:50:31 +08:00
HolographicHat
baaf4e8227 rollback 2023-10-13 21:22:54 +08:00
HolographicHat
f41fe6fb12 4.1
* #71
2023-09-28 18:48:00 +08:00
HolographicHat
78bda3f49c Merge pull request #70 from BTMuli/master
#67
2023-09-23 00:00:30 +08:00
BTMuli
a10dc22461 🎨 修正检测方式 2023-09-22 23:39:37 +08:00
HolographicHat
74dda750ef Merge pull request #69 from Masterain98/master
Update GitLab CI configuration
2023-09-22 21:15:21 +08:00
Masterain
099270ad29 Update .gitlab-ci.yml 2023-09-21 23:42:47 -07:00
Masterain
fe5b2c0c12 Update .gitlab-ci.yml 2023-09-21 23:35:17 -07:00
HolographicHat
ed5d99745c Merge pull request #68 from Masterain98/master
Create GitLab CI Config File
2023-09-21 23:46:58 +08:00
Masterain
7175cd7427 Create .gitlab-ci.yml 2023-09-21 00:01:13 -07:00
HolographicHat
73747bcce5 #67 2023-09-19 22:28:58 +08:00
HolographicHat
b12c3209d7 fix ci 2023-08-27 11:39:40 +08:00
HolographicHat
5805070627 4.0.1 (fix #64) 2023-08-24 00:16:44 +08:00
HolographicHat
811daba164 4.0 2023-08-16 20:00:06 +08:00
HolographicHat
849f379ca5 3.8 2023-07-06 14:31:55 +08:00
HolographicHat
29c57cbc1e Merge pull request #62 from Lightczx/master
Improve export experience for Snap Hutao
2023-06-29 21:30:58 +08:00
DismissedLight
8fdb10898a Improve export experience for Snap Hutao 2023-06-29 14:38:19 +08:00
HolographicHat
f4f1f14651 Merge pull request #61 from prpjzz/master
Add Readme_EN.md
2023-06-22 15:03:23 +08:00
prpjzz
390af6cd85 Update Tutorial_EN.md 2023-06-19 22:00:38 +07:00
prpjzz
a78f36dfa8 Update README_EN.md 2023-06-19 21:56:51 +07:00
prpjzz
e3d7152da2 Update Tutorial_EN.md 2023-06-19 21:14:20 +07:00
prpjzz
60a4dd04c4 Merge branch 'HolographicHat:master' into master 2023-06-19 18:49:45 +07:00
HolographicHat
69c402417b 3.7 2023-05-24 17:32:27 +08:00
prpjzz
d9034ec635 Update Tutorial_EN.md 2023-05-03 14:06:04 +07:00
prpjzz
f742cf37d5 Create Tutorial_EN.md 2023-05-03 13:27:26 +07:00
prpjzz
4136a1b0c4 Update README_EN.md 2023-05-03 13:26:38 +07:00
prpjzz
e039d2f944 Update README_EN.md 2023-05-03 13:26:21 +07:00
prpjzz
0e23a05a78 Update README_EN.md 2023-05-03 13:25:26 +07:00
prpjzz
5f5a0614a6 Update README_EN.md 2023-05-03 13:20:02 +07:00
HolographicHat
c60d3a3b82 v2.7 2023-04-12 12:28:03 +08:00
HolographicHat
58dcd5b228 Fix 2023-04-02 18:23:58 +08:00
HolographicHat
a72007ffa6 Merge remote-tracking branch 'origin/master' 2023-04-02 16:20:23 +08:00
HolographicHat
d3b9d10d01 #55 2023-04-02 16:17:11 +08:00
HolographicHat
69184fa59d Auto compile protos 2023-03-30 08:54:50 +08:00
HolographicHat
8473336b37 Update Tutorial.md 2023-03-01 20:57:42 +08:00
HolographicHat
ffc0854291 bump version to 2.6.0 2023-03-01 20:48:45 +08:00
HolographicHat
3b2b1fba49 fix ci 2023-02-28 19:59:46 +08:00
HolographicHat
c325b5f754 3.5 2023-02-28 19:16:51 +08:00
HolographicHat
8e2e438c96 fix ci 2023-02-27 20:01:23 +08:00
HolographicHat
9bc2cdb443 Update actions .net version 2023-02-27 19:47:06 +08:00
HolographicHat
826ed661cd Merge pull request #51 from Lightczx/master
Use Microsoft.Windows.CsWin32 to replace manual PInvoke
2023-02-27 19:45:11 +08:00
DismissedLight
5f8ff734f2 fixup native call 2023-02-26 14:41:24 +08:00
DismissedLight
50007c2c53 Use CsWin32 2023-02-26 13:46:03 +08:00
HolographicHat
7512c6fca2 Merge pull request #50 from peaceshi/master
open export dir.
2023-02-21 18:09:06 +08:00
peaceshi
2feae6ddb9 open export dir. 2023-02-20 18:13:05 +08:00
HolographicHat
52ae44f467 fix #49 2023-02-09 21:18:51 +08:00
HolographicHat
d93f6f92c0 Merge pull request #48 from xzm2000/patch-1
Update Tutorial.md
2023-01-25 22:52:54 +08:00
xzm2000
20b59eab7e Update Tutorial.md
update url
2023-01-25 11:23:51 +08:00
HolographicHat
0094b9b959 Update README.md 2023-01-19 15:08:33 +08:00
HolographicHat
24b68fbed1 Merge pull request #46 from Finchaos/patch-1
Create Tutorial.md
2023-01-19 15:04:04 +08:00
HolographicHat
397923d4ad Update and rename tutorial.md to Tutorial.md 2023-01-19 14:54:08 +08:00
Finchaos
0a3482e7b2 Create tutorial.md 2023-01-19 14:21:34 +08:00
HolographicHat
7ae18cfbf0 v2.5 2023-01-18 11:52:00 +08:00
HolographicHat
24cd49fa03 3.4 AchievementAllDataNotify 2023-01-18 02:54:42 +08:00
HolographicHat
f0dbb9162b Use 77.cocogoat.cn 2023-01-18 02:47:20 +08:00
HolographicHat
68ff9c5a25 3.4 native lib 2023-01-18 02:46:54 +08:00
HolographicHat
31b77a9fb3 fix empty result and bump version to 2.4.1 2022-12-07 23:36:18 +08:00
HolographicHat
d2d5bafcd6 v2.4 2022-12-07 13:02:22 +08:00
HolographicHat
e2f1f1e343 lib for 3.3 2022-12-07 12:13:27 +08:00
HolographicHat
349e15fe25 v2.3 2022-11-02 09:31:18 +08:00
HolographicHat
9e0d18910b improve user experience 2022-11-01 11:11:11 +08:00
HolographicHat
afee99fd3f native lib 3.2 2022-11-01 10:26:18 +08:00
HolographicHat
4dae8c52b6 Update actions/upload-artifact 2022-10-17 08:39:35 +08:00
HolographicHat
3294ac4b89 add try catch 2022-10-17 08:19:21 +08:00
HolographicHat
ccb19c832c ignore tmp file delete exception 2022-10-08 19:41:39 +08:00
HolographicHat
1207dd70b3 Add game executable hash check 2022-10-03 16:33:58 +08:00
HolographicHat
204f211249 bump version to 2.2.1 2022-10-02 13:20:18 +08:00
HolographicHat
043a861030 improve update 2022-10-02 13:19:54 +08:00
HolographicHat
09a9d4c22b fix crash 2022-10-02 00:39:45 +08:00
HolographicHat
9094b9c718 update 2022-10-01 20:03:03 +08:00
HolographicHat
d7ac18587a snapgenshin -> snaphutao 2022-09-29 21:03:44 +08:00
HolographicHat
82c5054002 bump version to 2.2 2022-09-28 11:13:43 +08:00
HolographicHat
553d67bff7 fix 2022-09-28 11:09:32 +08:00
HolographicHat
07a08f56d4 3.1 native lib 2022-09-27 19:02:13 +08:00
HolographicHat
656589bc80 add cmdline args 2022-09-27 18:16:52 +08:00
HolographicHat
9aaefe4cd7 snap.hutao 2022-09-27 16:58:46 +08:00
HolographicHat
d84571ba1c Fix and optimize game path detect 2022-09-25 22:36:29 +08:00
HolographicHat
9bc8d2473a improve Export.cs 2022-09-25 22:33:29 +08:00
HolographicHat
7b3e22c84f Fix path error 2022-09-25 21:04:18 +08:00
HolographicHat
faa583587a update dependencies 2022-09-25 13:34:30 +08:00
HolographicHat
37bfb93fa9 better exception message 2022-09-25 13:31:30 +08:00
HolographicHat
b88858d2dc Improve game path getter 2022-09-25 13:02:11 +08:00
HolographicHat
c2e3a3b13d auto install vcruntime 2022-09-25 12:29:10 +08:00
HolographicHat
78a29e9390 clear input buffer before select export way 2022-09-19 18:44:17 +08:00
HolographicHat
a76f03b035 Fix 2022-09-06 13:32:36 +08:00
HolographicHat
d814ece2de fix i18n 2022-09-04 19:02:06 +08:00
HolographicHat
9414ffbe12 Merge remote-tracking branch 'origin/master' 2022-09-02 20:24:21 +08:00
HolographicHat
251246fd74 Fix 2022-09-02 20:24:05 +08:00
HolographicHat
76785c5179 Update README_EN.md 2022-09-02 20:00:41 +08:00
HolographicHat
e086e14e8d Create build workflow 2022-09-02 19:50:41 +08:00
HolographicHat
29aa4fea2d Update csproj 2022-09-02 19:50:08 +08:00
HolographicHat
ffd75da2bf fix #31 2022-09-01 11:16:08 +08:00
HolographicHat
92e8cd8997 Merge remote-tracking branch 'origin/master' 2022-08-30 14:54:10 +08:00
HolographicHat
4b62901dbe UIAF update 2022-08-30 14:53:53 +08:00
HolographicHat
2216610413 Update Readme 2022-08-27 13:17:27 +00:00
HolographicHat
99b8db2a69 Merge pull request #26 from prpjzz/master
Update readme_en.md and make some changes to the original readme file
2022-08-27 20:41:27 +08:00
prpjzz
5b6e86459d Delete icon.png 2022-08-27 19:25:30 +07:00
prpjzz
bf46494b6b Update README.md 2022-08-27 19:24:58 +07:00
prpjzz
458a56a855 Create README_EN.md 2022-08-27 19:23:34 +07:00
prpjzz
ef21274cd2 Add files via upload 2022-08-27 19:22:52 +07:00
prpjzz
7528f4247b Add files via upload 2022-08-27 19:22:29 +07:00
HolographicHat
652b5afa80 Update README.md 2022-08-27 15:21:52 +08:00
HolographicHat
59a042019a Add en lang 2022-08-27 15:08:01 +08:00
HolographicHat
4e94d67d0b Bump version to 2.1 2022-08-24 17:03:06 +08:00
HolographicHat
7dafd95099 Merge remote-tracking branch 'origin/master' 2022-08-24 17:02:53 +08:00
HolographicHat
76c20407ea Update README.md 2022-08-23 00:00:33 +08:00
HolographicHat
b79b82ec10 Merge pull request #24 from xunkong/hat
Add Xunkong and adapt to UIAF 1.1
2022-08-22 23:59:59 +08:00
Scighost
07fe60a092 Add Xunkong and adapt to UIAF 1.1 2022-08-22 23:37:27 +08:00
HolographicHat
7fa2fbac25 3.0 offsets 2022-08-22 14:25:35 +08:00
HolographicHat
28ffa6fb1a UIAF 1.1 2022-08-21 12:10:47 +08:00
HolographicHat
4c2cb28313 add export 2022-08-21 12:06:50 +08:00
HolographicHat
2f1a5ad99e #23 2022-08-19 18:09:49 +08:00
HolographicHat
9b0c384d4b Add AppConfig 2022-08-19 18:09:30 +08:00
HolographicHat
b2111db4eb Implement #22 2022-08-15 23:36:23 +08:00
HolographicHat
2442264224 Add CacheFile 2022-08-15 23:35:29 +08:00
HolographicHat
b596cad02e Remove unused code 2022-08-15 23:34:47 +08:00
HolographicHat
30a0189f5e Merge remote-tracking branch 'origin/master' 2022-08-14 21:42:38 +08:00
HolographicHat
f47fd234b4 add vcruntime check 2022-08-14 21:42:21 +08:00
HolographicHat
7d306a60c9 Update README.md 2022-08-14 21:28:30 +08:00
HolographicHat
10dd03335f lib update 2022-08-12 23:34:04 +08:00
HolographicHat
41863c32f7 optimize 2022-07-16 01:41:11 +08:00
HolographicHat
4c3e9d8e50 Fix os2.8 crash 2022-07-15 23:02:00 +08:00
HolographicHat
9d60cda4c7 Merge pull request #19 from huiyadanli/master
Add feat: get game path from registry (YuanShen.exe)
2022-07-15 22:03:55 +08:00
huiyadanli
e0c836e55d Add feat: get game path from registry (YuanShen.exe) 2022-07-15 21:51:36 +08:00
HolographicHat
02b4d9df0b Update README.md 2022-07-15 16:34:36 +08:00
HolographicHat
d5a20b44d5 2.8 2022-07-14 16:47:05 +08:00
HolographicHat
31e23de4d6 remove files 2022-07-13 20:42:24 +08:00
HolographicHat
fb8c941b57 implement appcenter and minor fix 2022-07-13 20:38:09 +08:00
HolographicHat
468b4b91ea Fix dialog 2022-07-13 20:38:06 +08:00
HolographicHat
0ace6b951f wait rel 2.8 2022-07-13 20:38:04 +08:00
HolographicHat
74473a3811 Implement export: snapgenshin and cocogoat 2022-07-13 20:38:01 +08:00
HolographicHat
34afe3b7a4 Implement export: paimon and csv 2022-07-13 20:37:59 +08:00
HolographicHat
f1e8c09262 Update .gitattributes 2022-07-13 20:37:56 +08:00
HolographicHat
6a1fbe8fff Update AchievementAllDataNotify 2022-07-13 20:37:52 +08:00
HolographicHat
e33ee57afc update 2022-07-13 20:37:50 +08:00
HolographicHat
54763cbc80 update feat 2022-07-13 20:37:45 +08:00
HolographicHat
1dcaf7ed8f http client with cache 2022-07-13 20:37:43 +08:00
HolographicHat
bbe7c2cd03 update 2022-07-13 20:37:39 +08:00
HolographicHat
e847d4c80e optimize code and AchievementAllDataNotify 2022-07-13 20:37:37 +08:00
HolographicHat
f753acfc78 packet whitelist 2022-07-13 20:37:35 +08:00
HolographicHat
08ccdb203e update 2022-07-13 20:37:32 +08:00
HolographicHat
180ab8bab7 update 2022-07-13 20:37:31 +08:00
HolographicHat
1adc6c4c0f simple test 2022-07-13 20:37:28 +08:00
HolographicHat
82ab5a316c Implement il2cpp initialize method 2022-07-13 20:37:26 +08:00
HolographicHat
75c2f57cfa il2cpp function&type and hook manager 2022-07-13 20:37:23 +08:00
HolographicHat
ef6a72312c optimize code and update 2022-07-13 20:37:21 +08:00
HolographicHat
3917f3e6c7 lib update 2022-07-13 20:37:18 +08:00
HolographicHat
12348a3941 win32 define and injector 2022-07-13 20:37:15 +08:00
HolographicHat
8aed3fd095 license and readme 2022-07-13 20:37:00 +08:00
HolographicHat
4110fc2d6d Initial commit 2022-07-13 20:36:43 +08:00
HolographicHat
0de747e344 Delete achievement-data.json 2022-05-31 12:46:04 +08:00
HolographicHat
b76e8e3cd2 Fix 2022-05-31 11:31:00 +08:00
HolographicHat
c12d376e21 Support rel2.7.0 and bump version to 1.7 2022-05-31 11:05:33 +08:00
HolographicHat
d285b1c999 Update message 2022-05-29 22:33:34 +08:00
HolographicHat
89ab4408d6 Update 2022-05-23 12:14:24 +08:00
HolographicHat
bf01214971 Merge pull request #12 from HolographicHat/no-client
Update
2022-05-19 22:07:08 +08:00
HolographicHat
1900937a50 Update 2022-05-19 22:04:23 +08:00
HolographicHat
98a03a0910 Fix #11 2022-04-25 16:16:13 +08:00
HolographicHat
82ba6120e4 snap update 2022-04-23 16:07:13 +08:00
HolographicHat
6cb07d274a UIAF: Update field name 2022-04-21 19:40:17 +08:00
HolographicHat
4acaa516bb UIAF Support 2022-04-21 15:11:05 +08:00
HolographicHat
b5caba2f7a Merge pull request #10 from gaoyifan/master
Fix paimon.moe exporting format
2022-04-18 18:42:43 +08:00
Yifan Gao
ebb11bde9c Fix paimon.moe exporting format 2022-04-18 04:29:34 +08:00
HolographicHat
4417feab53 encode uri to avoid charset problem 2022-04-17 23:46:11 +08:00
HolographicHat
28080dbafd fix 2022-04-17 22:54:35 +08:00
HolographicHat
a8b6419157 Update README.md 2022-04-16 21:52:50 +08:00
HolographicHat
7f8296c3dc fix bug and bump version to 1.6 2022-04-16 21:07:00 +08:00
HolographicHat
37382b28e0 auto package script 2022-04-16 20:59:32 +08:00
HolographicHat
a46e49722f auto package script 2022-04-16 00:35:41 +08:00
HolographicHat
700cbbb86d update 2022-04-13 20:54:00 +08:00
HolographicHat
6a23153f70 add oversea(NA-SiliconValley) api 2022-04-13 19:34:14 +08:00
HolographicHat
75e3cd848f update 2022-04-12 20:00:12 +08:00
HolographicHat
96912d3da7 support gbk encoding 2022-04-10 14:18:01 +08:00
HolographicHat
02b034cc48 Merge remote-tracking branch 'origin/master' 2022-04-10 13:03:08 +08:00
HolographicHat
00898d11cb fix 2022-04-10 13:02:36 +08:00
HolographicHat
7cf03ad905 Update README.md 2022-04-10 02:03:08 +08:00
HolographicHat
7d9e5bc218 Update README.md 2022-04-09 20:37:27 +08:00
HolographicHat
60f0d8d23b fix appcenter log upload error 2022-04-09 19:33:52 +08:00
HolographicHat
1e15a49667 new format 2022-04-09 16:35:46 +08:00
HolographicHat
4528af7235 rollback 2022-04-09 00:30:16 +08:00
HolographicHat
9850d1dbe4 bump version to 1.5 2022-04-09 00:19:15 +08:00
HolographicHat
b1f307de83 snapgenshin format update 2022-04-08 23:56:43 +08:00
HolographicHat
bea596b906 fetch achievement data from self api server 2022-04-08 23:56:20 +08:00
HolographicHat
dd582437bc use fastest cdn to fetch data 2022-04-07 22:50:36 +08:00
HolographicHat
ea168ce96b support snapgenshin 2022-04-07 21:56:27 +08:00
HolographicHat
90ab4dafe9 speed test file 2022-04-07 21:26:09 +08:00
HolographicHat
55f1ce3d55 auto export to cocogoat 2022-04-07 21:04:49 +08:00
HolographicHat
0e51e080d4 no hard code loc 2022-04-07 20:13:42 +08:00
HolographicHat
01ab053d7d Update README.md 2022-04-06 22:18:26 +08:00
HolographicHat
41af9c7cdb better pause impl 2022-04-06 13:05:20 +08:00
HolographicHat
8a0c82f89f catch wmi error 2022-04-06 11:26:05 +08:00
HolographicHat
d64bf8149e Update README.md 2022-04-06 08:53:10 +08:00
HolographicHat
82e85f5ea0 v1.4 2022-04-06 00:15:42 +08:00
HolographicHat
891a19c3f7 select game by open file dialog, optimize code and message 2022-04-06 00:14:48 +08:00
HolographicHat
7b94964342 more message and check, better privilege test 2022-04-06 00:13:03 +08:00
HolographicHat
d5e290e866 modify and add some message, optimize 2022-04-06 00:11:21 +08:00
HolographicHat
f7c48472f1 native: enablePrivilege and copyToClipboard 2022-04-06 00:03:12 +08:00
HolographicHat
a6b80d3588 more error check 2022-04-05 20:13:52 +08:00
HolographicHat
18e3d3ffe3 fix #5 2022-04-05 19:11:12 +08:00
HolographicHat
a22ad8e87d Merge remote-tracking branch 'origin/master' 2022-04-05 18:53:42 +08:00
HolographicHat
35ec8b5859 native: import wmi as submodule 2022-04-05 18:53:09 +08:00
HolographicHat
35899e74f6 Update README.md 2022-04-05 02:23:54 +08:00
HolographicHat
bb4d5215f1 native: getDeviceID and getDeviceInfo 2022-04-05 02:22:01 +08:00
HolographicHat
e4e76286c9 native addon 2022-04-04 22:50:59 +08:00
HolographicHat
be5a457639 native addon 2022-04-04 17:08:47 +08:00
HolographicHat
945a94222a Update README.md 2022-04-03 23:40:26 +08:00
HolographicHat
3d03496074 Update messages 2022-04-02 22:48:51 +08:00
HolographicHat
1e6ebc76dd Update README.md 2022-04-02 22:37:15 +08:00
HolographicHat
ab47ff2b06 fix crash when hosts not exist 2022-04-01 20:31:49 +08:00
HolographicHat
5fe6e80cc8 Merge remote-tracking branch 'origin/master' 2022-03-30 14:10:23 +08:00
HolographicHat
a23d7f3cc9 more handler 2022-03-30 14:09:50 +08:00
HolographicHat
0ab6444b6f more message 2022-03-29 22:33:24 +08:00
HolographicHat
00f0aaa4a6 2.6 Achievement data 2022-03-28 18:30:08 +08:00
HolographicHat
2377dbd492 Update achievement-data.json 2022-03-28 18:26:00 +08:00
HolographicHat
0b015886ea Update README.md 2022-03-28 16:32:28 +08:00
HolographicHat
4adfc9a312 Update README.md 2022-03-27 13:04:14 +08:00
HolographicHat
b4cd69f303 v1.3 2022-03-27 12:51:41 +08:00
HolographicHat
c76ddd9e3f fix wrong field name 2022-03-27 12:47:25 +08:00
HolographicHat
d40c456494 add default arg for read registry method 2022-03-26 20:51:54 +08:00
HolographicHat
408169da4e Merge remote-tracking branch 'origin/master' 2022-03-26 17:55:57 +08:00
HolographicHat
9de8e957fd add server disconnect handler 2022-03-26 16:59:21 +08:00
HolographicHat
a287e5db43 Update README.md 2022-03-26 16:39:43 +08:00
HolographicHat
589048b912 Update README.md 2022-03-26 16:39:14 +08:00
HolographicHat
79517a37d2 try to fix copy error 2022-03-25 21:53:14 +08:00
HolographicHat
e022f04661 fix achievement data error 2022-03-25 21:33:46 +08:00
HolographicHat
d1a635be7c fix exit hook 2022-03-25 21:32:43 +08:00
HolographicHat
aa853262b7 Merge remote-tracking branch 'origin/master' 2022-03-25 19:23:18 +08:00
HolographicHat
bdf9561dfa fix crash when appcenter error 2022-03-25 19:22:34 +08:00
HolographicHat
a663fddeb0 Update README.md 2022-03-23 20:58:21 +08:00
59 changed files with 3934 additions and 4771 deletions

21
.gitattributes vendored Normal file
View File

@@ -0,0 +1,21 @@
lib/src/il2cpp-types.h 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
View 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: 8.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

20
.gitignore vendored
View File

@@ -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
View 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

View File

@@ -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/>

View File

@@ -1,40 +1,42 @@
# 原神成就导出工具
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/icon.ico">
![GitHub](https://img.shields.io/github/license/HolographicHat/genshin-achievement-export?label=License&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/genshin-achievement-export?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/genshin-achievement-export/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
# YaeAchievement
- 支持导出所有成就
- 没有窗口大小游戏或语言等要求
- 更快、更准
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
简体中文 | [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 文件
## 使用说明
**打开程序前需要关闭正在运行的原神主程序**
第一次打开需要先设置原神主程序所在路径,支持多个路径, 使用符号'*'分隔
![alt](https://upload-bbs.mihoyo.com/upload/2022/03/22/165631158/a1bbf8d0604a29830c09822add53f749_8463600217231045373.png)
- 自定义代理: 配置文件内添加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
}
}
```
→ [Tutorial.md](Tutorial.md)
## 下载地址
[releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest)
[releases/latest](https://github.com/HolographicHat/YaeAchievement/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)
[issues](https://github.com/HolographicHat/YaeAchievement/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下找到
## 常见问题
0. Q: 打不开
A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer)
## TODO
- [ ] GUI
1. Q: 原神启动时报错: 数据异常(31-4302)
A: 不要把软件和原神主程序放一起

44
README_EN.md Normal file
View File

@@ -0,0 +1,44 @@
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/icon.ico">
# YaeAchievement
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
[简体中文](README.md) | English
</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.

81
Tutorial.md Normal file
View File

@@ -0,0 +1,81 @@
## 使用说明
1.选择正确的下载文件以2.4.1版本为例):
点击该网址https://github.com/HolographicHat/YaeAchievement/releases
点击图中红框圈中的名称为“YaeAchievement.exe”的文件浏览器会自动弹出下载。建议将该文件保存在桌面或者其它易于寻找的文件夹内。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/1.png)
2.安装启动软件所需文件(若已安装该运行时可忽略此步骤)
点击该网址https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer 。
进入网页后浏览器会自动弹出下载,同样地,将文件保存在桌面或者其它易于寻找的文件夹内。
下载完成后打开名称形如dotnet-runtime-x.x.x-win-x64.exe的文件会弹出安装窗口如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/2.png)
直接点击安装即可。
3.打开主程序所需的操作以及成就导出的选择
双击在第一步下载的名称为“YaeAchievement.exe”的文件成功打开后会提示原神正在启动如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/3.png)
原神启动完成后,点击进入游戏即可。
点击进入游戏后原神闪退,工具会提示您选择导出至何种工具,如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/4.png)
此时可根据自己的需要进行选择,一般推荐导出至[0]椰羊以及[4]表格文件(.csv
选择完毕后各工具导出页面如下:
#### 椰羊:
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/5.png)
#### Snap.Hutao
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main//images/6.png)
#### Seelie.me
此时YaeAchievement会提示成就数据已导出。请在保存YaeAchievement的文件夹内找到名称形如export-20xxxxxxxxxxxx-seelie.json的文件。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/7.png)
然后点击该网址https://seelie.me/settings, 进入网页后选择导入,如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/8.png)
点击导入后选中名称形如export-20xxxxxxxxxxxx-seelie.json的文件如下图所示。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/9.png)
当弹出如下图所示的提示时表示导入成功。
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/10.png)
此时可选择左栏成就,查看导入的成就数据。
#### 寻空:
![image](https://github.com/Finchaos/yae-markdown-230119/blob/main/images/11.png)
### 各种工具的介绍烦请移步至各工具的官方页面进行查看(下方序号对应导出序号)
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 文件

76
Tutorial_EN.md Normal file
View File

@@ -0,0 +1,76 @@
## Instructions for Use
1.Download YaeAchievementLatest Version
Click Herehttps://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.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide1.png)
2.Install .NET Runtime 7 (this step can be ignored if the runtime is already installed)
Click Herehttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer .
Or `winget install Microsoft.DotNet.Runtime.7` if you use Windows 11 or have Winget installed.
The browser automatically pops up and downloads when you enter the web page, as well as saving files in a desktop or other easy-to-see folder.
When you open a file with the name dotnet-runtime-x.x.x-win-x64.exe after the download is complete, an installation window pops up, as shown below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide2.png)
Just click Install.
3.The actions required to open the main program and the options for the achievement export
Double-click the file named "YaeAchievement.exe" downloaded in the first step to open it successfully, indicating that the original god is starting, as shown below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide3.png)
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.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide4.png)
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
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide5.png)
#### 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.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide6.png)
Then click on the URL: https://seelie.me/settings, enter the website and select Import Account, as illustrated in the figure below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide7.png)
After clicking Import, select a file named export-20xxxxxxxxxxxx-seelie.json, as shown in the figure below.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide8.png)
When the prompt as shown in the image below pops up, the import process succeeds.
![image](https://github.com/prpjzz/yae-markdown-en/blob/main/image/Guide9.png)
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`~~
5. [XunKong](https://github.com/xunkong/xunkong)
6. [YuanmoTools](https://apps.apple.com/app/id1663989619)
7. [Teyvat Guide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON file

61
YaeAchievement.csproj Normal file
View File

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

16
YaeAchievement.sln Normal file
View File

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

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

File diff suppressed because it is too large Load Diff

149
app.js
View File

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

View File

@@ -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
}

Binary file not shown.

View File

@@ -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
View File

@@ -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
}

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

4
lib/.gitignore vendored Normal file
View File

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

29
lib/YaeAchievementLib.sln Normal file
View 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

View File

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

Binary file not shown.

1227
lib/lib/detours/detours.h Normal file

File diff suppressed because it is too large Load Diff

27
lib/lib/detours/detver.h Normal file
View File

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

66
lib/src/HookManager.h Normal file
View File

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

88
lib/src/dllmain.cpp Normal file
View File

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

11
lib/src/il2cpp-appdata.h Normal file
View File

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

View File

@@ -0,0 +1,9 @@
using namespace Genshin;
// DO_APP_FUNC(CN_OFFSET, OS_OFFSET, RETURN, FUNC_NAME, (ARGS...));
DO_APP_FUNC(0x005DDC40, 0x006A2A90, ByteArray*, il2cpp_array_new_specific, (void* arrayTypeInfo, uint64_t length));
DO_APP_FUNC(0x06F7D5B0, 0x06C68BE0, ByteArray*, RecordUserData, (int32_t nType));
DO_APP_FUNC(0x0D1C10F0, 0x0D1BA490, uint16_t, BitConverter_ToUInt16, (ByteArray* val, int startIndex));

34
lib/src/il2cpp-init.cpp Normal file
View File

@@ -0,0 +1,34 @@
// ReSharper disable CppCStyleCast
// ReSharper disable CppInconsistentNaming
// ReSharper disable CppClangTidyBugproneMacroParentheses
// ReSharper disable CppClangTidyClangDiagnosticCastAlign
#include "pch.h"
#include "il2cpp-init.h"
#define DO_APP_FUNC(ca, oa, r, n, p) r (*n) p
namespace Genshin {
#include "il2cpp-functions.h"
}
#undef DO_APP_FUNC
using std::string;
void InitIL2CPP(void* &ppRecordUserData) {
TCHAR szFileName[MAX_PATH];
GetModuleFileName(nullptr, szFileName, MAX_PATH);
const auto isCN = strstr(szFileName, "YuanShen.exe");
const auto uBase = reinterpret_cast<uint64_t>(GetModuleHandle("UserAssembly.dll"));
#define DO_APP_FUNC(ca, oa, r, n, p) n = (r (*) p)(uBase + (isCN ? ca : oa))
#include "il2cpp-functions.h"
#undef DO_APP_FUNC
auto sPtr = reinterpret_cast<uint8_t*>(RecordUserData);
for (int i = 0; i < 0x64; ++i) {
if ((*(uint32_t*) sPtr & 0xFFFFFF) == 0x25FF48) { // 48 FF 25 ??
ppRecordUserData = sPtr + 7 + *(int*) (sPtr + 3);
break;
}
sPtr += 1;
}
}

4
lib/src/il2cpp-init.h Normal file
View File

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

15
lib/src/il2cpp-types.h Normal file
View File

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

1
lib/src/pch.cpp Normal file
View File

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

23
lib/src/pch.h Normal file
View File

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

93
lib/src/util.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "pch.h"
#include "util.h"
VOID DisableVMProtect() {
DWORD oldProtect = 0;
auto ntdll = GetModuleHandleA("ntdll.dll");
auto pNtProtectVirtualMemory = GetProcAddress(ntdll, "NtProtectVirtualMemory");
auto pNtQuerySection = GetProcAddress(ntdll, "NtQuerySection");
DWORD old;
VirtualProtect(pNtProtectVirtualMemory, 1, PAGE_EXECUTE_READWRITE, &old);
*(uintptr_t*)pNtProtectVirtualMemory = *(uintptr_t*)pNtQuerySection & ~(0xFFui64 << 32) | (uintptr_t)(*(uint32_t*)((uintptr_t)pNtQuerySection + 4) - 1) << 32;
VirtualProtect(pNtProtectVirtualMemory, 1, old, &old);
}
#pragma region ByteUtils
bool IsLittleEndian() {
UINT i = 1;
char* c = (char*)&i;
return *c;
}
#pragma endregion
#pragma region FindMainWindowByPID
struct HandleData {
DWORD pid;
HWND hwnd;
};
BOOL IsMainWindow(HWND handle) {
return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle) == TRUE;
}
BOOL IsUnityWindow(HWND handle) {
TCHAR name[256];
GetClassName(handle, name, 256);
return _strcmpi(name, "UnityWndClass") == 0;
}
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
HandleData& data = *(HandleData*)lParam;
DWORD pid = 0;
GetWindowThreadProcessId(handle, &pid);
if (data.pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle))
return TRUE;
data.hwnd = handle;
return FALSE;
}
HWND FindMainWindowByPID(DWORD pid) {
HandleData data = { pid, 0 };
EnumWindows(EnumWindowsCallback, (LPARAM)&data);
return data.hwnd;
}
#pragma endregion
static const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string base64_encode(BYTE const* buf, unsigned int bufLen) {
std::string ret;
int i = 0;
BYTE char_array_3[3];
BYTE char_array_4[4];
while (bufLen--) {
char_array_3[i++] = *buf++;
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; (i < 4); i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
int j;
for (j = i; j < 3; j++)
char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (j = 0; j < i + 1; j++)
ret += base64_chars[char_array_4[j]];
while (i++ < 3)
ret += '=';
}
return ret;
}

25
lib/src/util.h Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
using std::string;
VOID DisableVMProtect();
bool IsLittleEndian();
HWND FindMainWindowByPID(DWORD pid);
std::string base64_encode(BYTE const* buf, unsigned int bufLen);
#define ErrorDialogT(title, msg) MessageBox(unityWnd, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL)
#define ErrorDialog(msg) ErrorDialogT("YaeAchievement", msg)
#define Win32ErrorDialog(code) ErrorDialogT("YaeAchievement", ("CRITICAL ERROR!\nError code: " + std::to_string(GetLastError()) + "-"#code"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!").c_str())
template<class T>
static T ReadMapped(void* data, int offset, bool littleEndian = false) {
char* cData = (char*)data;
T result = {};
if (IsLittleEndian() != littleEndian) {
for (int i = 0; i < sizeof(T); i++)
((char*)&result)[i] = cData[offset + sizeof(T) - i - 1];
return result;
}
memcpy(&result, cData + offset, sizeof(result));
return result;
}

View File

@@ -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"
}
}

View File

@@ -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;
}

View File

@@ -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
}

397
res/App.Designer.cs generated Normal file
View File

@@ -0,0 +1,397 @@
//------------------------------------------------------------------------------
// <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} =&gt; {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);
}
}
}
}

140
res/App.resx Normal file
View File

@@ -0,0 +1,140 @@
<?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} =&gt; {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>
</root>

130
res/App.zh.resx Normal file
View File

@@ -0,0 +1,130 @@
<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} =&gt; {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>
</root>

BIN
res/Updater.exe Normal file

Binary file not shown.

78
res/app.manifest Normal file
View 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>

View File

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

View File

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

10
res/proto/CacheItem.proto Normal file
View 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;
}

View File

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

66
src/AppConfig.cs Normal file
View File

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

39
src/CacheFile.cs Normal file
View File

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

263
src/Export.cs Normal file
View File

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

41
src/Extensions.cs Normal file
View 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);
}
}

33
src/GlobalVars.cs Normal file
View File

@@ -0,0 +1,33 @@
using System.Reflection;
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 DebugProxy => false;
public static bool UnexpectedExit { get; set; } = true;
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 = 47;
public const string AppVersionName = "3.7";
public const string PipeName = "YaeAchievementPipe";
public const string BucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
static GlobalVars() {
Directory.CreateDirectory(DataPath);
Directory.CreateDirectory(CachePath);
}
}

62
src/Injector.cs Normal file
View File

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

6
src/NativeMethods.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"className": "Native",
"allowMarshaling": false,
"public": true
}

21
src/NativeMethods.txt Normal file
View 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

50
src/Program.cs Normal file
View File

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

318
src/Utils.cs Normal file
View File

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

293
utils.js
View File

@@ -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
}

View File

@@ -1,6 +0,0 @@
const version = {
code: 2,
name: "1.0.1"
}
module.exports = { version }