341 Commits
1.4 ... 5.7.3

Author SHA1 Message Date
HolographicHat
ccff7a1702 [skip ci] bump lib version 2025-10-23 08:05:27 +08:00
HolographicHat
79122d6ba7 bump version 2025-10-23 08:02:06 +08:00
HolographicHat
1218d1cbc4 fix #146, #147 2025-10-23 08:01:43 +08:00
HolographicHat
19f84bdb86 bump version 2025-10-22 06:23:04 +08:00
HolographicHat
9d22fdf6f9 implement WhenFirstSuccessful method for improved task handling 2025-10-22 06:21:22 +08:00
HolographicHat
27fa0d9c84 move srcgen project 2025-10-22 01:46:58 +08:00
HolographicHat
e9baf8f211 [skip ci] update ci 2025-10-18 20:40:18 +08:00
HolographicHat
b960165b7e Merge pull request #144 from Lightczx/master 2025-10-14 16:17:21 +08:00
DismissedLight
a4c2027ada code style 2025-10-14 13:44:51 +08:00
DismissedLight
f49477c49a Generated MinHook.Attach methods 2025-10-14 11:30:58 +08:00
HolographicHat
67c2fb3bda bump lib version 2025-10-10 18:20:13 +08:00
HolographicHat
be3440695d auto enter gate (close #143) 2025-09-16 11:42:49 +08:00
HolographicHat
f8b8a5a9e1 #142 2025-08-30 11:39:41 +08:00
HolographicHat
49f8679996 bump version 2025-08-05 16:46:56 +08:00
HolographicHat
222a26233e mirror 2025-08-05 01:45:02 +08:00
HolographicHat
1130f442fc update native lib 2025-08-05 00:10:13 +08:00
HolographicHat
b80987f574 [skip ci] update ci 2025-08-04 22:38:29 +08:00
HolographicHat
c96395e1a2 call awake in wndhook 2025-08-04 22:27:48 +08:00
HolographicHat
3f42156b20 ReadAtLeast 2025-08-04 16:19:33 +08:00
HolographicHat
45638b7327 update ci 2025-08-03 14:13:08 +08:00
HolographicHat
d514f3b5e7 [skip ci] update ci 2025-08-03 13:51:55 +08:00
HolographicHat
3fe54d908e add wndhook 2025-08-03 13:46:09 +08:00
HolographicHat
48043a9deb remove test code 2025-08-02 11:26:03 +08:00
HolographicHat
c09e008d38 bump version 2025-08-02 11:20:10 +08:00
HolographicHat
ac818735c7 suppress pipe broken exception 2025-08-02 08:18:09 +08:00
HolographicHat
c4fb5a0366 invoke libMain by CRT
Co-authored-by: HolographicHat <holographichat@outlook.com>
Co-authored-by: 34736384 <34736384@users.noreply.github.com>
2025-08-02 07:48:44 +08:00
HolographicHat
0522078582 bump version 2025-08-02 02:17:36 +08:00
HolographicHat
97d063ec76 update dependencies 2025-08-02 02:05:14 +08:00
HolographicHat
a6caa07599 update ci & remove old native lib 2025-08-02 02:03:04 +08:00
HolographicHat
7fc296f6e9 refactor native lib 2025-08-02 01:58:01 +08:00
HolographicHat
1216ffb872 [skip ci] update join group link 2025-08-01 15:34:24 +08:00
HolographicHat
f8341c3044 Update ci 2025-06-25 01:04:25 +08:00
HolographicHat
40269410c5 fix sentry#2707 2025-06-23 00:03:12 +08:00
HolographicHat
a32e27323c fix sentry#2715 2025-06-22 23:15:34 +08:00
HolographicHat
4a1da61904 bump version 2025-06-18 14:44:37 +08:00
HolographicHat
9eb8955fda optimize GetGamePath 2025-06-11 01:47:34 +08:00
HolographicHat
62c08f54ab fix 2025-06-08 00:44:32 +08:00
HolographicHat
645fe38c65 fix #138 2025-06-07 20:35:24 +08:00
HolographicHat
8f9a26a237 fix #2471 2025-06-05 21:56:03 +08:00
HolographicHat
8648b3a308 bump version 2025-06-04 03:16:09 +08:00
HolographicHat
829553b3a6 sentry 2025-06-04 03:14:07 +08:00
HolographicHat
87898eedfa fix 2025-06-04 02:01:00 +08:00
HolographicHat
a10b491886 prefer using field id from config 2025-05-30 01:23:39 +08:00
HolographicHat
e3e7107b14 Update YaeAchievementLib.nuspec 2025-05-08 10:11:05 +08:00
HolographicHat
3231746aa5 Merge pull request #135 from 34736384/master 2025-05-07 15:44:48 +08:00
34736384
5c9cdd46d2 update psn pattern 2025-05-07 13:34:52 +08:00
HolographicHat
881a4bc725 [no ci] update docs 2025-04-13 03:49:58 +08:00
HolographicHat
d08ac17d10 bump version 2025-04-13 03:14:07 +08:00
HolographicHat
980a47bf43 spectre console 2025-04-11 23:57:07 +08:00
HolographicHat
0e7be25b23 allow copy cocogoat import url 2025-04-09 01:10:21 +08:00
HolographicHat
4b052cf6c7 refactor CacheFile 2025-04-09 01:09:47 +08:00
HolographicHat
4ff2b454f3 refactor updater and injector 2025-04-08 23:15:53 +08:00
HolographicHat
f718687b3f [no ci] update feedback group id 2025-04-06 15:15:36 +08:00
HolographicHat
9915b9246e disable CETCompat 2025-04-05 21:47:59 +08:00
HolographicHat
e25bc9aeba Merge pull request #133 from BTMuli/master
✏️ Typo uigf to uiaf
2025-04-01 15:50:28 +08:00
目棃
08dd6eca76 ✏️ Typo uigf to uiaf 2025-04-01 15:21:04 +08:00
HolographicHat
acd2ccd803 Merge pull request #132 from Lightczx/master
Inject side check ignore case
2025-03-28 11:15:43 +08:00
DismissedLight
13fda3ba12 bump nuspec version 2025-03-28 10:59:25 +08:00
DismissedLight
1c821620bf Inject side check ignore case 2025-03-28 10:58:39 +08:00
HolographicHat
8a39ad0a77 [skip ci] update readme 2025-03-27 16:24:00 +08:00
HolographicHat
9d42141258 bump lib version 2025-03-26 16:22:59 +08:00
HolographicHat
2a91656b2e [skip ci] update ci 2025-03-26 16:19:42 +08:00
HolographicHat
602cf06a8b bump version 2025-03-26 16:03:26 +08:00
HolographicHat
c87b8c976d update 2025-03-26 16:02:48 +08:00
HolographicHat
099c22e9e7 Merge pull request #130 from 34736384/master
update resolver
2025-03-26 15:00:05 +08:00
REL
f1fe6e1f9e update resolver 2025-03-26 02:45:33 -04:00
HolographicHat
fb0a46480f [no ci] bump version 2025-02-12 17:35:57 +08:00
HolographicHat
7b413384c3 Merge pull request #129 from 34736384/master
fix player store id resolver
2025-02-12 17:27:43 +08:00
REL
e251497edc fix player store id resolver 2025-02-12 04:24:43 -05:00
HolographicHat
b1f5d9a2b2 [skip ci] Merge pull request #128 from qhy040404/nuget 2025-01-27 21:14:55 +08:00
qhy040404
36a5b9a0e7 fix ci 2025-01-27 21:13:19 +08:00
HolographicHat
b0b70d585a [skip_ci] Merge pull request #127 from qhy040404/nuget 2025-01-27 21:04:42 +08:00
qhy040404
b9ab326d72 workflow_dispatch 2025-01-27 20:02:21 +08:00
qhy040404
5f210d4236 publish lib to nuget 2025-01-27 19:36:08 +08:00
HolographicHat
826063da60 [skip ci] Merge pull request #126 from qhy040404/patch 2025-01-26 13:43:33 +08:00
qhy040404
08697941d3 print cocogoat url 2025-01-26 12:02:39 +08:00
HolographicHat
e7d21865c7 [skip ci] support virtual item 2025-01-20 17:32:47 +08:00
HolographicHat
35773f49f4 [skip ci] Merge pull request #125 from Lightczx/master 2025-01-20 14:45:18 +08:00
DismissedLight
c4856c821f Fix inject side check 2025-01-20 11:08:04 +08:00
HolographicHat
6f1168e61e [skip ci] Merge pull request #124 from qhy040404/windowhook 2025-01-17 22:13:43 +08:00
qhy040404
d1bd0c7d7b fix 2025-01-17 21:55:50 +08:00
qhy040404
95ad187015 add pin to avoid unload 2025-01-17 21:40:59 +08:00
HolographicHat
6c0264ce5a Merge pull request #123 from qhy040404/windowhook 2025-01-17 17:13:59 +08:00
qhy040404
957c8b98e4 add window hook entry 2025-01-17 17:11:25 +08:00
REL
01a3f41323 [skip ci] fix 2025-01-11 19:51:13 +08:00
HolographicHat
32ceae074e [skip ci] export player store data 2025-01-11 19:24:42 +08:00
HolographicHat
2e2be07161 update docs 2025-01-10 16:23:36 +08:00
HolographicHat
832c82f44e Merge pull request #122 from 34736384/master 2025-01-10 14:42:02 +08:00
REL
43b38df986 fix bugs 2025-01-09 01:05:46 -05:00
REL
6e1c8f275f refactor 2025-01-07 23:56:16 -05:00
REL
e65f046520 added PlayerStoreNotify 2025-01-07 23:36:27 -05:00
HolographicHat
4025729677 ci 2025-01-08 09:32:51 +08:00
HolographicHat
07050c1c3d move sources 2025-01-08 09:30:17 +08:00
REL
e56a6228aa use cpp23 and 'modernize' 2025-01-07 20:16:14 -05:00
HolographicHat
66b29b1374 static linking 2024-11-30 14:13:51 +08:00
HolographicHat
247c401a5b aot 2024-11-30 13:52:39 +08:00
HolographicHat
cf9d601b27 rin 2024-11-30 11:56:41 +08:00
HolographicHat
5abb9e2934 Merge pull request #118 from Mikachu2333/master 2024-11-22 13:07:50 +08:00
HolographicHat
1b861712eb fix #119 #120 2024-11-21 21:35:41 +08:00
LinkChou
d62377ad96 prepare for next version 2024-11-21 10:31:53 +08:00
HolographicHat
fb4e3f8d00 fix 2024-11-21 08:11:26 +08:00
HolographicHat
b1135542c1 5.2 2024-11-21 07:33:07 +08:00
HolographicHat
7d3d0f5e14 auto identify field ids 2024-11-21 07:31:21 +08:00
HolographicHat
4268b04f3c ignore zydis 2024-10-15 20:41:04 +08:00
HolographicHat
9a9d1310a1 Merge pull request #115 from 34736384/master 2024-10-15 20:37:59 +08:00
REL
618a9189ad use sse 2024-10-15 07:32:14 -04:00
REL
f73dbdc4fe switch to amalgamated zydis 2024-10-14 20:24:32 -04:00
REL
298134c063 added dynamic resolving of CmdId and ToUInt16 2024-10-14 05:54:32 -04:00
REL
e9ace26d69 refactor 2024-10-10 05:06:57 -04:00
REL
2c15353f86 remove detours 2024-10-09 08:32:36 -04:00
REL
99fec63867 remove il2cpp bloat 2024-10-09 08:14:41 -04:00
HolographicHat
bf5525d2ea 5.1 2024-10-09 16:03:25 +08:00
HolographicHat
cf3749f887 fix #114 2024-09-12 23:47:01 +08:00
HolographicHat
21af4de1a6 5.0 2024-08-29 18:24:21 +08:00
HolographicHat
8e0fd2d27c fix #111 2024-08-05 18:11:51 +08:00
HolographicHat
0348cfa365 Merge pull request #107 from eltociear/add-japanese-readme-tutorial 2024-07-31 07:58:19 +08:00
Ikko Eltociear Ashimine
494eda32c2 docs: add Japanese README and tutorial 2024-07-23 00:47:25 +09:00
HolographicHat
975638c1ee Merge pull request #104 from canmengxian/master 2024-07-17 18:33:19 +08:00
HolographicHat
793ad075fe 4.8 2024-07-17 18:12:24 +08:00
残梦
c82a10353f Update Utils.cs 2024-06-23 23:16:13 +08:00
HolographicHat
f737122247 4.7 2024-06-06 02:07:56 +08:00
HolographicHat
520167ef85 fix #82 #101 2024-04-29 14:38:13 +08:00
HolographicHat
faee6f6121 Update README_EN.md 2024-04-25 21:50:15 +08:00
HolographicHat
06c5468118 update readme 2024-04-25 21:42:14 +08:00
HolographicHat
b7c2204f68 Update CI 2024-04-25 02:52:46 +08:00
HolographicHat
5dc5e646d6 fix #87 2024-04-25 02:45:30 +08:00
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
73 changed files with 4253 additions and 5394 deletions

21
.gitattributes vendored Normal file
View File

@@ -0,0 +1,21 @@
lib/src/Zydis.* linguist-generated=true
# Auto detect text files and perform LF normalization
* text=auto
*.cs text diff=csharp
*.cshtml text diff=html
*.csx text diff=csharp
*.sln text eol=crlf merge=union
*.csproj text merge=union
# Sources
*.c text diff=cpp
*.cc text diff=cpp
*.cxx text diff=cpp
*.cpp text diff=cpp
*.c++ text diff=cpp
*.hpp text diff=cpp
*.h text diff=cpp
*.h++ text diff=cpp
*.hh text diff=cpp

40
.github/workflows/dotnet.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: .NET Build
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: windows-latest
defaults:
run:
working-directory: ./YaeAchievement
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build -c Release --no-restore
- name: Publish-AOT
run: dotnet publish --property:OutputPath=.\publish\
- name: Upload-AOT
uses: actions/upload-artifact@v4
with:
name: aot
path: YaeAchievement\publish\publish
- name: Publish-NoAOT
run: dotnet publish --property:OutputPath=.\naot-publish\ --property:PublishAot=false --property:PublishSingleFile=true --property:PublishTrimmed=true
- name: Upload-NoAOT
uses: actions/upload-artifact@v4
with:
name: normal
path: YaeAchievement\naot-publish\publish

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

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

15
.gitignore vendored
View File

@@ -1,11 +1,6 @@
cache
cert
config.json
out.*
node_modules
bin
obj
.idea
app*.exe
export*.json
secret.js
package-lock.json
native.node
.vs
publish
sync

64
.gitlab-ci.yml Normal file
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

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "native/src/wmi"]
path = native/src/wmi
url = https://github.com/HolographicHat/wmi

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,51 +1,37 @@
# 原神成就导出工具
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?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)
- 支持导出所有成就
- 支持官服B服与国际服
- 没有窗口大小游戏或语言等要求
- 更快、更准
## 使用说明
**打开程序前需要关闭正在运行的原神主程序**
第一次打开需要先设置原神主程序(YuanShen.exe/GenshinImpact.exe)所在路径
![alt](https://upload-bbs.mihoyo.com/upload/2022/04/06/165631158/e540a5a6d50cd5fdee19665435548e00_514247033566841954.jpg)
### Windows7
系统变量添加名为```NODE_SKIP_PLATFORM_CHECK```的变量并将值设为```1```
### 自定义代理(可选)
配置文件内添加proxy字段详细请参看[Axios-请求配置](https://axios-http.com/zh/docs/req_config)
```json
{
"path": [],
"offlineResource": false,
"customCDN": "",
"proxy": {
"protocol": "http",
"host": "127.0.0.1",
"port": 7890
}
}
```
## 下载地址
[releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest)
## 问题反馈
[issues](https://github.com/HolographicHat/genshin-achievement-export/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## 常见问题
0. Q: 程序异常退出或被强行终止后,原神启动时报错: 无法连接网络(4201)
A: 用文本编辑器打开```C:\Windows\System32\drivers\etc\hosts```,删除```127.0.0.1 dispatch**global.yuanshen.com```后保存,重启原神
1. Q: 原神启动时报错: 数据异常(31-4302)
A: 不要把软件和原神主程序放一起
2. Q: 为什么需要管理员权限
A: 临时修改Hosts和启动原神
3. Q: 报毒?
A: 执行命令或修改hosts引发相关代码可在./utils.js,./appcenter.js下找到
4. Q: 原神进门后没有自动退出,程序输出停留在“加载完成”
A: 关闭Shadowsocks全局代理
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
# YaeAchievement
![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) | [日本語](README_JP.md)
</div>
- 支持导出所有类别的成就
- 支持官服,渠道服与国际服
- 没有窗口大小、游戏语言等要求
## 导出支持
0. [椰羊](https://cocogoat.work/achievement)
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements)
4. 表格文件 `.csv`
5. [寻空](https://github.com/xunkong/xunkong)
6. [原魔工具箱](https://apps.apple.com/app/id1663989619)
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON 文件
## 使用说明
→ [Tutorial.md](Tutorial.md)
## 下载地址
[releases/latest](https://github.com/HolographicHat/YaeAchievement/releases/latest)
## 问题反馈
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
## 常见问题
1. Q: 原神启动时报错: 数据异常(31-4302)
A: 不要把软件和原神主程序放一起

38
README_EN.md Normal file
View File

@@ -0,0 +1,38 @@
<div align="center"><img width="100" src="https://github.com/HolographicHat/YaeAchievement/blob/master/YaeAchievement/res/icon.ico" alt="AppIcon">
# YaeAchievement
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
[简体中文](README.md) | English | [日本語](README_JP.md)
</div>
- Support for exporting all categories of achievements
- Supports all versions of Genshin Impact
- There are no requirements for window size, game language, etc.
## Export support
0. [Cocogoat](https://cocogoat.work/achievement)
1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements)
4. Form File `.csv`
5. [XunKong](https://github.com/xunkong/xunkong)
6. [YuanmoTools](https://apps.apple.com/app/id1663989619)
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON file
## Instructions for Use:
→ [Tutorial_EN.md](Tutorial_EN.md)
## Download: [Here](https://github.com/HolographicHat/YaeAchievement/releases/latest)
## Feedback or Problem?
[issues](https://github.com/HolographicHat/YaeAchievement/issues) or [QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
## Frequently asked questions
1. Q: Error while Genshin started: Data Exception (31-4302)
A: Don't place software in the directory containing Genshin Impact.

38
README_JP.md Normal file
View File

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

69
Tutorial.md Normal file
View File

@@ -0,0 +1,69 @@
## 使用说明
1.选择正确的下载文件以2.4.1版本为例):
点击该网址https://github.com/HolographicHat/YaeAchievement/releases
点击图中红框圈中的名称为“YaeAchievement.exe”的文件浏览器会自动弹出下载。建议将该文件保存在桌面或者其它易于寻找的文件夹内。
![1](https://github.com/user-attachments/assets/8b98c018-b179-4681-992d-367a0f522dae)
2.打开主程序所需的操作以及成就导出的选择
双击在第一步下载的名称为“YaeAchievement.exe”的文件成功打开后会提示原神正在启动如下图所示。
![3](https://github.com/user-attachments/assets/3d9eb78b-187f-4ada-90b7-5a951c9d0ee1)
原神启动完成后,点击进入游戏即可。
点击进入游戏后原神自动退出,工具会提示您选择导出至何种工具,如下图所示。
![4](https://github.com/user-attachments/assets/76b64d42-2865-4f61-9ceb-8e317af50e7e)
此时可根据自己的需要进行选择,一般推荐导出至[0]椰羊以及[4]表格文件(.csv
选择完毕后各工具导出页面如下:
#### 椰羊:
![5](https://github.com/user-attachments/assets/9e3188ab-cfad-4cfc-8db9-51ae22ff7caa)
#### Snap.Hutao
![6](https://github.com/user-attachments/assets/8d8cabe1-1ebc-4329-898b-0c725a5b10e4)
#### Seelie.me
此时YaeAchievement会提示成就数据已导出。请在保存YaeAchievement的文件夹内找到名称形如export-20xxxxxxxxxxxx-seelie.json的文件。
![7](https://github.com/user-attachments/assets/c0c6724e-90cd-4e58-b441-a3af97493455)
然后点击该网址https://seelie.me/settings, 进入网页后选择导入,如下图所示。
![8](https://github.com/user-attachments/assets/4182f3dd-723d-452b-aa32-2246ad710ac1)
点击导入后选中名称形如export-20xxxxxxxxxxxx-seelie.json的文件如下图所示。
![9](https://github.com/user-attachments/assets/9466ee0b-a5e9-4bf5-9f95-5ac86ea0c8f6)
当弹出如下图所示的提示时表示导入成功。
![A](https://github.com/user-attachments/assets/755193d6-41f6-4108-b9ca-867e92107a03)
此时可选择左栏成就,查看导入的成就数据。
#### 寻空:
![B](https://github.com/user-attachments/assets/f78c9a70-0b81-4c19-a034-5ed9f8e6eff4)
### 各种工具的介绍烦请移步至各工具的官方页面进行查看(下方序号对应导出序号)
0. [椰羊](https://cocogoat.work/achievement)
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements)
4. [表格文件 `.csv`](https://en.wikipedia.org/wiki/Comma-separated_values)
5. [寻空](https://github.com/xunkong/xunkong)
6. [原魔工具箱](https://apps.apple.com/app/id1663989619)
7. [TeyvatGuide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON 文件

62
Tutorial_EN.md Normal file
View File

@@ -0,0 +1,62 @@
# Instructions for Use
1.Download YaeAchievementLatest Version
Click Here<https://github.com/HolographicHat/YaeAchievement/releases>
Click on the file named "YaeAchievement.exe" in the red box to automatically pop up and download.It is recommended that
you save this file in a desktop or other easy-to-see folder.
![Step1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3)
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.
![Guide3](https://github.com/user-attachments/assets/c3375188-1fa3-4a0b-9007-358afbcaae91)
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.
![Guide4](https://github.com/user-attachments/assets/c806582a-3608-4c86-af26-ce8e631ff610)
For global user, you should select [3] Seelie.me or [4] Export to csv file。
After selecting, each page exports the tool as follows:
#### Snap.Hutao
![Guide5](https://github.com/user-attachments/assets/40d547d8-fe04-4462-8b78-284394a44c36)
#### 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.
![Guide6](https://github.com/user-attachments/assets/91cdb0e6-883d-4f5e-9f1f-416eb8c16433)
Then click on the URL: https://seelie.me/settings, enter the website and select Import Account, as illustrated in the figure below.
![Guide7](https://github.com/user-attachments/assets/e6a9ddb1-b075-4f0b-9e42-a1d61b4808bc)
After clicking Import, select a file named export-20xxxxxxxxxxxx-seelie.json, as shown in the figure below.
![Guide8](https://github.com/user-attachments/assets/1b7edb51-ff0d-415c-bd96-0e6c9ac7a238)
When the prompt as shown in the image below pops up, the import process succeeds.
![Guide9](https://github.com/user-attachments/assets/e155b4e5-ce15-4dd8-9633-a83b9759bce1)
At this time, you can select the Achievements in the left column to view the imported performance data.
### For the introduction of different tools, please visit the official page of each tool to see:
0. [Cocogoat](https://cocogoat.work/achievement)
1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/)
3. [Seelie.me](https://seelie.me/achievements)
4. [Form File `.csv`](https://en.wikipedia.org/wiki/Comma-separated_values)
5. [XunKong](https://github.com/xunkong/xunkong)
6. [YuanmoTools](https://apps.apple.com/app/id1663989619)
7. [Teyvat Guide](https://github.com/BTMuli/TeyvatGuide)
8. [UIAF](https://uigf.org/standards/UIAF.html) JSON file

61
Tutorial_JP.md Normal file
View File

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

View File

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

View File

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

5
YaeAchievement.slnx Normal file
View File

@@ -0,0 +1,5 @@
<Solution>
<Project Path="YaeAchievement.SourceGeneration/YaeAchievement.SourceGeneration.csproj" />
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" />
<Project Path="YaeAchievement\YaeAchievement.csproj" />
</Solution>

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFramework>net9.0-windows</TargetFramework>
<FileVersion>5.7.2</FileVersion>
<AssemblyVersion>5.7.2</AssemblyVersion>
<ApplicationIcon>res\icon.ico</ApplicationIcon>
<ApplicationManifest>res\app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<OptimizationPreference>Size</OptimizationPreference>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- [Update to 3.31.0 breaks AOT build](https://github.com/protocolbuffers/protobuf/issues/21824) -->
<PackageReference Include="Google.Protobuf" Version="3.30.2"/>
<PackageReference Include="Grpc.Tools" Version="2.72.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Sentry" Version="5.13.0"/>
<PackageReference Include="Spectre.Console" Version="0.50.1-preview.0.22"/>
<PackageReference Include="Spectre.Console.Analyzer" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="res\App.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>App.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Remove="res\updater.exe"/>
<EmbeddedResource Include="res\updater.exe" LogicalName="updater"/>
</ItemGroup>
<ItemGroup>
<Compile Update="res\App.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>App.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None"/>
</ItemGroup>
<PropertyGroup>
<CETCompat>false</CETCompat>
<!-- <TrimmerSingleWarn>false</TrimmerSingleWarn>-->
</PropertyGroup>
</Project>

558
YaeAchievement/res/App.Designer.cs generated Normal file
View File

@@ -0,0 +1,558 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace YaeAchievement.res {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class App {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal App() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("YaeAchievement.res.App", typeof(App).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to all achievement.
/// </summary>
internal static string AllAchievement {
get {
return ResourceManager.GetString("AllAchievement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please close another instance..
/// </summary>
internal static string AnotherInstance {
get {
return ResourceManager.GetString("AnotherInstance", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to YaeAchievement ({0}).
/// </summary>
internal static string AppBanner {
get {
return ResourceManager.GetString("AppBanner", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No.
/// </summary>
internal static string CommonNo {
get {
return ResourceManager.GetString("CommonNo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Yes.
/// </summary>
internal static string CommonYes {
get {
return ResourceManager.GetString("CommonYes", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please launch GenshinImpact to continue..
/// </summary>
internal static string ConfigNeedStartGenshin {
get {
return ResourceManager.GetString("ConfigNeedStartGenshin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Download: {0}.
/// </summary>
internal static string DownloadLink {
get {
return ResourceManager.GetString("DownloadLink", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Network error ({0}: {1}).
/// </summary>
internal static string ExceptionNetwork {
get {
return ResourceManager.GetString("ExceptionNetwork", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Export to:.
/// </summary>
internal static string ExportChoose {
get {
return ResourceManager.GetString("ExportChoose", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cocogoat (https://cocogoat.work/achievement).
/// </summary>
internal static string ExportTargetCocogoat {
get {
return ResourceManager.GetString("ExportTargetCocogoat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Csv file.
/// </summary>
internal static string ExportTargetCsv {
get {
return ResourceManager.GetString("ExportTargetCsv", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Snap.HuTao.
/// </summary>
internal static string ExportTargetHuTao {
get {
return ResourceManager.GetString("ExportTargetHuTao", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Paimon.moe.
/// </summary>
internal static string ExportTargetPaimon {
get {
return ResourceManager.GetString("ExportTargetPaimon", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Seelie.me.
/// </summary>
internal static string ExportTargetSeelie {
get {
return ResourceManager.GetString("ExportTargetSeelie", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Teyvat Guide.
/// </summary>
internal static string ExportTargetTeyvatGuide {
get {
return ResourceManager.GetString("ExportTargetTeyvatGuide", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to UIAF JSON File.
/// </summary>
internal static string ExportTargetUIAFJson {
get {
return ResourceManager.GetString("ExportTargetUIAFJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string ExportTargetWxApp1 {
get {
return ResourceManager.GetString("ExportTargetWxApp1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Xunkong.
/// </summary>
internal static string ExportTargetXunkong {
get {
return ResourceManager.GetString("ExportTargetXunkong", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fail, please contact developer to get help information (CG_{0}).
/// </summary>
internal static string ExportToCocogoatFail {
get {
return ResourceManager.GetString("ExportToCocogoatFail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to cocogoat..
/// </summary>
internal static string ExportToCocogoatSuccess {
get {
return ResourceManager.GetString("ExportToCocogoatSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to {0}.
/// </summary>
internal static string ExportToFileSuccess {
get {
return ResourceManager.GetString("ExportToFileSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update Snap Hutao and retry..
/// </summary>
internal static string ExportToSnapGenshinNeedUpdate {
get {
return ResourceManager.GetString("ExportToSnapGenshinNeedUpdate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to Snap Hutao..
/// </summary>
internal static string ExportToSnapGenshinSuccess {
get {
return ResourceManager.GetString("ExportToSnapGenshinSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please launch/update Teyvat Guide and retry..
/// </summary>
internal static string ExportToTauriFail {
get {
return ResourceManager.GetString("ExportToTauriFail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to Teyvat Guide..
/// </summary>
internal static string ExportToTauriSuccess {
get {
return ResourceManager.GetString("ExportToTauriSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.
/// </summary>
internal static string ExportToWxApp1Success {
get {
return ResourceManager.GetString("ExportToWxApp1Success", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update xunkong and retry..
/// </summary>
internal static string ExportToXunkongNeedUpdate {
get {
return ResourceManager.GetString("ExportToXunkongNeedUpdate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to xunkong..
/// </summary>
internal static string ExportToXunkongSuccess {
get {
return ResourceManager.GetString("ExportToXunkongSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game process start ({0}).
/// </summary>
internal static string GameLoading {
get {
return ResourceManager.GetString("GameLoading", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game exited..
/// </summary>
internal static string GameProcessExit {
get {
return ResourceManager.GetString("GameProcessExit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update genshin and retry..
/// </summary>
internal static string GenshinHashError {
get {
return ResourceManager.GetString("GenshinHashError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please close game before run this application. ({0}).
/// </summary>
internal static string GenshinIsRunning {
get {
return ResourceManager.GetString("GenshinIsRunning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Load failed. Please retry. ({0} {1} {2}).
/// </summary>
internal static string LoadLibraryFail {
get {
return ResourceManager.GetString("LoadLibraryFail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Network error:.
/// </summary>
internal static string NetworkError {
get {
return ResourceManager.GetString("NetworkError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No write permission on {0}..
/// </summary>
internal static string NoWritePermission {
get {
return ResourceManager.GetString("NoWritePermission", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Press any key to exit..
/// </summary>
internal static string PressKeyToExit {
get {
return ResourceManager.GetString("PressKeyToExit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use the keyboard arrow keys to move the cursor and the Enter key to select.
/// </summary>
internal static string SelectionPromptCompatAnsiTip {
get {
return ResourceManager.GetString("SelectionPromptCompatAnsiTip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose an option:.
/// </summary>
internal static string SelectionPromptCompatChooseOne {
get {
return ResourceManager.GetString("SelectionPromptCompatChooseOne", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please enter a number between 0 and {0}.
/// </summary>
internal static string SelectionPromptCompatInvalidChoice {
get {
return ResourceManager.GetString("SelectionPromptCompatInvalidChoice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Type a number and press Enter to select.
/// </summary>
internal static string SelectionPromptCompatNonAnsiTip {
get {
return ResourceManager.GetString("SelectionPromptCompatNonAnsiTip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reward not taken.
/// </summary>
internal static string StatusFinished {
get {
return ResourceManager.GetString("StatusFinished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid.
/// </summary>
internal static string StatusInvalid {
get {
return ResourceManager.GetString("StatusInvalid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Finished.
/// </summary>
internal static string StatusRewardTaken {
get {
return ResourceManager.GetString("StatusRewardTaken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unfinished.
/// </summary>
internal static string StatusUnfinished {
get {
return ResourceManager.GetString("StatusUnfinished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An error occurred while reading the data. Please try again..
/// </summary>
internal static string StreamReadDataFail {
get {
return ResourceManager.GetString("StreamReadDataFail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Checking update....
/// </summary>
internal static string UpdateChecking {
get {
return ResourceManager.GetString("UpdateChecking", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Description:
///{0}.
/// </summary>
internal static string UpdateDescription {
get {
return ResourceManager.GetString("UpdateDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Downloading update package....
/// </summary>
internal static string UpdateDownloading {
get {
return ResourceManager.GetString("UpdateDownloading", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The process cannot access the file &apos;{0}&apos; because it is being used by another process. Please restart your computer and try again..
/// </summary>
internal static string UpdateFileShareViolation {
get {
return ResourceManager.GetString("UpdateFileShareViolation", resourceCulture);
}
}
/// <summary>
/// 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 string similar to Upload error to appcenter....
/// </summary>
internal static string UploadError {
get {
return ResourceManager.GetString("UploadError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use previous fetched data?.
/// </summary>
internal static string UsePreviousData {
get {
return ResourceManager.GetString("UsePreviousData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Downloading Visual C++ Redistributable....
/// </summary>
internal static string VcRuntimeDownload {
get {
return ResourceManager.GetString("VcRuntimeDownload", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installing Visual C++ Redistributable....
/// </summary>
internal static string VcRuntimeInstalling {
get {
return ResourceManager.GetString("VcRuntimeInstalling", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update game and retry..
/// </summary>
internal static string WaitMetadataUpdate {
get {
return ResourceManager.GetString("WaitMetadataUpdate", resourceCulture);
}
}
}
}

187
YaeAchievement/res/App.resx Normal file
View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ExportToCocogoatFail" xml:space="preserve">
<value>Fail, please contact developer to get help information (CG_{0})</value>
</data>
<data name="AllAchievement" xml:space="preserve">
<value>all achievement</value>
</data>
<data name="ExportChoose" xml:space="preserve">
<value>Export to:</value>
</data>
<data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>Successfully exported to cocogoat.</value>
</data>
<data name="ExportToWxApp1Success" xml:space="preserve">
<value>{0}</value>
</data>
<data name="ExportToSnapGenshinSuccess" xml:space="preserve">
<value>Successfully exported to Snap Hutao.</value>
</data>
<data name="ExportToSnapGenshinNeedUpdate" xml:space="preserve">
<value>Please update Snap Hutao and retry.</value>
</data>
<data name="ExportToFileSuccess" xml:space="preserve">
<value>Successfully exported to {0}</value>
</data>
<data name="ExportToXunkongSuccess" xml:space="preserve">
<value>Successfully exported to xunkong.</value>
</data>
<data name="ExportToXunkongNeedUpdate" xml:space="preserve">
<value>Please update xunkong and retry.</value>
</data>
<data name="StatusInvalid" xml:space="preserve">
<value>Invalid</value>
</data>
<data name="StatusRewardTaken" xml:space="preserve">
<value>Finished</value>
</data>
<data name="StatusUnfinished" xml:space="preserve">
<value>Unfinished</value>
</data>
<data name="StatusFinished" xml:space="preserve">
<value>Reward not taken</value>
</data>
<data name="ConfigNeedStartGenshin" xml:space="preserve">
<value>Please launch GenshinImpact to continue.</value>
</data>
<data name="DownloadLink" xml:space="preserve">
<value>Download: {0}</value>
</data>
<data name="GameProcessExit" xml:space="preserve">
<value>Game exited.</value>
</data>
<data name="GameLoading" xml:space="preserve">
<value>Game process start ({0})</value>
</data>
<data name="UploadError" xml:space="preserve">
<value>Upload error to appcenter...</value>
</data>
<data name="PressKeyToExit" xml:space="preserve">
<value>Press any key to exit.</value>
</data>
<data name="GenshinIsRunning" xml:space="preserve">
<value>Please close game before run this application. ({0})</value>
</data>
<data name="AnotherInstance" xml:space="preserve">
<value>Please close another instance.</value>
</data>
<data name="UpdateNewVersion" xml:space="preserve">
<value>Has update: {0} =&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?</value>
</data>
<data name="NetworkError" xml:space="preserve">
<value>Network error:</value>
</data>
<data name="VcRuntimeDownload" xml:space="preserve">
<value>Downloading Visual C++ Redistributable...</value>
</data>
<data name="VcRuntimeInstalling" xml:space="preserve">
<value>Installing Visual C++ Redistributable...</value>
</data>
<data name="ExceptionNetwork" xml:space="preserve">
<value>Network error ({0}: {1})</value>
</data>
<data name="GenshinHashError" xml:space="preserve">
<value>Please update genshin and retry.</value>
</data>
<data name="NoWritePermission" xml:space="preserve">
<value>No write permission on {0}.</value>
</data>
<data name="ExportToTauriSuccess" xml:space="preserve">
<value>Successfully exported to Teyvat Guide.</value>
</data>
<data name="ExportToTauriFail" xml:space="preserve">
<value>Please launch/update Teyvat Guide and retry.</value>
</data>
<data name="WaitMetadataUpdate" xml:space="preserve">
<value>Please update game and retry.</value>
</data>
<data name="UpdateChecking" xml:space="preserve">
<value>Checking update...</value>
</data>
<data name="CommonYes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="CommonNo" xml:space="preserve">
<value>No</value>
</data>
<data name="ExportTargetCocogoat" xml:space="preserve">
<value>Cocogoat (https://cocogoat.work/achievement)</value>
</data>
<data name="ExportTargetHuTao" xml:space="preserve">
<value>Snap.HuTao</value>
</data>
<data name="ExportTargetPaimon" xml:space="preserve">
<value>Paimon.moe</value>
</data>
<data name="ExportTargetSeelie" xml:space="preserve">
<value>Seelie.me</value>
</data>
<data name="ExportTargetCsv" xml:space="preserve">
<value>Csv file</value>
</data>
<data name="ExportTargetXunkong" xml:space="preserve">
<value>Xunkong</value>
</data>
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
<value>Teyvat Guide</value>
</data>
<data name="ExportTargetUIAFJson" xml:space="preserve">
<value>UIAF JSON File</value>
</data>
<data name="ExportTargetWxApp1" xml:space="preserve">
<value />
</data>
<data name="SelectionPromptCompatChooseOne" xml:space="preserve">
<value>Choose an option:</value>
</data>
<data name="SelectionPromptCompatAnsiTip" xml:space="preserve">
<value>Use the keyboard arrow keys to move the cursor and the Enter key to select</value>
</data>
<data name="SelectionPromptCompatNonAnsiTip" xml:space="preserve">
<value>Type a number and press Enter to select</value>
</data>
<data name="SelectionPromptCompatInvalidChoice" xml:space="preserve">
<value>Please enter a number between 0 and {0}</value>
</data>
<data name="StreamReadDataFail" xml:space="preserve">
<value>An error occurred while reading the data. Please try again.</value>
</data>
<data name="UpdateFileShareViolation" xml:space="preserve">
<value>The process cannot access the file '{0}' because it is being used by another process. Please restart your computer and try again.</value>
</data>
<data name="LoadLibraryFail" xml:space="preserve">
<value>Load failed. Please retry. ({0} {1} {2})</value>
</data>
</root>

View File

@@ -0,0 +1,180 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ExportToCocogoatFail" xml:space="preserve">
<value>导出失败, 请联系开发者以获取帮助CG_{0}</value>
</data>
<data name="AllAchievement" xml:space="preserve">
<value>全部成就</value>
</data>
<data name="ExportChoose" xml:space="preserve">
<value>要导出到哪里?</value>
</data>
<data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>在浏览器内进行下一步操作</value>
</data>
<data name="ExportToWxApp1Success" xml:space="preserve">
<value>在小程序导入页面输入以下代码: {0}</value>
</data>
<data name="ExportToSnapGenshinSuccess" xml:space="preserve">
<value>在 Snap Hutao 中进行下一步操作</value>
</data>
<data name="ExportToSnapGenshinNeedUpdate" xml:space="preserve">
<value>更新/安装 Snap Hutao 最新版本后重试</value>
</data>
<data name="ExportToFileSuccess" xml:space="preserve">
<value>成就数据已导出至 {0}</value>
</data>
<data name="ExportToXunkongSuccess" xml:space="preserve">
<value>在寻空中进行下一步操作</value>
</data>
<data name="ExportToXunkongNeedUpdate" xml:space="preserve">
<value>更新寻空至最新版本后重试</value>
</data>
<data name="StatusInvalid" xml:space="preserve">
<value>未知</value>
</data>
<data name="StatusFinished" xml:space="preserve">
<value>已完成但未领取奖励</value>
</data>
<data name="StatusUnfinished" xml:space="preserve">
<value>未完成</value>
</data>
<data name="StatusRewardTaken" xml:space="preserve">
<value>已完成</value>
</data>
<data name="ConfigNeedStartGenshin" xml:space="preserve">
<value>请打开 原神 后继续操作</value>
</data>
<data name="DownloadLink" xml:space="preserve">
<value>下载地址: {0}</value>
</data>
<data name="GameProcessExit" xml:space="preserve">
<value>游戏进程异常退出</value>
</data>
<data name="GameLoading" xml:space="preserve">
<value>原神正在启动 ({0})</value>
</data>
<data name="UploadError" xml:space="preserve">
<value>正在上报错误信息...</value>
</data>
<data name="PressKeyToExit" xml:space="preserve">
<value>按任意键退出</value>
</data>
<data name="GenshinIsRunning" xml:space="preserve">
<value>原神正在运行,请关闭后重试 ({0})</value>
</data>
<data name="AnotherInstance" xml:space="preserve">
<value>另一个实例正在运行,请关闭后重试</value>
</data>
<data name="UpdateNewVersion" xml:space="preserve">
<value>有可用更新: {0} =&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>要使用上一次获取到的成就数据吗?</value>
</data>
<data name="NetworkError" xml:space="preserve">
<value>网络错误: {0}</value>
</data>
<data name="VcRuntimeDownload" xml:space="preserve">
<value>正在下载 Visual C++ Redistributable...</value>
</data>
<data name="VcRuntimeInstalling" xml:space="preserve">
<value>正在安装 Visual C++ Redistributable...</value>
</data>
<data name="ExceptionNetwork" xml:space="preserve">
<value>网络错误,请检查网络后重试 ({0}: {1})</value>
</data>
<data name="GenshinHashError" xml:space="preserve">
<value>当前适配版本不匹配,请更新原神至最新版本后重试或等待工具更新。</value>
</data>
<data name="NoWritePermission" xml:space="preserve">
<value>无法写入文件,请更换软件所在目录后重试</value>
</data>
<data name="ExportToTauriFail" xml:space="preserve">
<value>启动 Teyvat Guide 或更新 Teyvat Guide 至最新版本后重试</value>
</data>
<data name="ExportToTauriSuccess" xml:space="preserve">
<value>在 Teyvat Guide 进行下一步操作</value>
</data>
<data name="WaitMetadataUpdate" xml:space="preserve">
<value>当前元数据版本不匹配,请更新原神至最新版本或等待元数据更新后重试。</value>
</data>
<data name="UpdateChecking" xml:space="preserve">
<value>正在检查更新...</value>
</data>
<data name="CommonYes" xml:space="preserve">
<value>是</value>
</data>
<data name="CommonNo" xml:space="preserve">
<value>否</value>
</data>
<data name="ExportTargetCocogoat" xml:space="preserve">
<value>椰羊 (https://cocogoat.work/achievement)</value>
</data>
<data name="ExportTargetHuTao" xml:space="preserve">
<value>Snap Hutao</value>
</data>
<data name="ExportTargetPaimon" xml:space="preserve">
<value>Paimon.moe</value>
</data>
<data name="ExportTargetSeelie" xml:space="preserve">
<value>Seelie.me</value>
</data>
<data name="ExportTargetCsv" xml:space="preserve">
<value>表格文件</value>
</data>
<data name="ExportTargetXunkong" xml:space="preserve">
<value>寻空</value>
</data>
<data name="ExportTargetUIAFJson" xml:space="preserve">
<value>UIAF JSON 文件</value>
</data>
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
<value>Teyvat Guide</value>
</data>
<data name="ExportTargetWxApp1" xml:space="preserve">
<value>原魔工具箱</value>
</data>
<data name="SelectionPromptCompatChooseOne" xml:space="preserve">
<value>选择一个选项:</value>
</data>
<data name="SelectionPromptCompatAnsiTip" xml:space="preserve">
<value>键盘上下键移动光标,键盘回车键选择</value>
</data>
<data name="SelectionPromptCompatNonAnsiTip" xml:space="preserve">
<value>输入数字并回车以选择选项</value>
</data>
<data name="SelectionPromptCompatInvalidChoice" xml:space="preserve">
<value>请输入 0 到 {0} 之间的数字</value>
</data>
<data name="StreamReadDataFail" xml:space="preserve">
<value>读取数据时发生错误,请重试</value>
</data>
<data name="UpdateFileShareViolation" xml:space="preserve">
<value>文件 {0} 被其它程序占用,请 重启电脑 或 解除文件占用 后重试。</value>
</data>
<data name="LoadLibraryFail" xml:space="preserve">
<value>加载失败,请重试({0} {1} {2}</value>
</data>
</root>

Binary file not shown.

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>

BIN
YaeAchievement/res/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -0,0 +1,45 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message AchievementProtoFieldInfo {
uint32 id = 1;
uint32 status = 2;
uint32 total_progress = 3;
uint32 current_progress = 4;
uint32 finish_timestamp = 5;
}
message AchievementItem {
uint32 pre = 1;
uint32 group = 2;
string name = 3;
string description = 4;
}
message MethodRvaConfig {
uint32 do_cmd = 1;
uint32 update_normal_prop = 3;
uint32 new_string = 4;
uint32 find_game_object = 5;
uint32 event_system_update = 6;
uint32 simulate_pointer_click = 7;
uint32 to_int32 = 8;
uint32 tcp_state_ptr = 9;
uint32 shared_info_ptr = 10;
uint32 decompress = 11;
}
message NativeLibConfig {
uint32 store_cmd_id = 1;
uint32 achievement_cmd_id = 2;
map<uint32, MethodRvaConfig> method_rva = 10;
}
message AchievementInfo {
string version = 1;
map<uint32, string> group = 2;
map<uint32, AchievementItem> items = 3;
AchievementProtoFieldInfo pb_info = 4;
NativeLibConfig native_config = 5;
}

View File

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

View File

@@ -0,0 +1,20 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message CdnFileInfo {
uint32 size = 1;
uint32 hash = 2;
repeated string urls = 4;
}
message UpdateInfo {
uint32 version_code = 1;
string version_name = 2;
string description = 3;
string package_link = 4;
bool force_update = 5;
bool enable_lib_download = 6;
bool enable_auto_update = 7;
map<string, CdnFileInfo> cdn_files = 8;
}

View File

@@ -0,0 +1,90 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.RegularExpressions;
using Spectre.Console;
using YaeAchievement.Utilities;
namespace YaeAchievement;
public static partial class AppConfig {
public static string GamePath { get; private set; } = null!;
private static readonly string[] ProductNames = [ "原神", "Genshin Impact" ];
internal static void Load(string argumentPath) {
if (argumentPath != "auto" && File.Exists(argumentPath)) {
GamePath = argumentPath;
} else if (TryReadGamePathFromCache(out var cachedPath)) {
GamePath = cachedPath;
} else if (TryReadGamePathFromUnityLog(out var loggedPath)) {
GamePath = loggedPath;
} else {
GamePath = ReadGamePathFromProcess();
}
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{Utils.GetGameHash(GamePath)}"));
SentrySdk.AddBreadcrumb(GamePath.EndsWith("YuanShen.exe") ? "CN" : "OS", "GamePath");
return;
static bool TryReadGamePathFromCache([NotNullWhen(true)] out string? path) {
path = null;
try {
if (!CacheFile.TryRead("genshin_impact_game_path_v2", out var cacheFile)) {
return false;
}
var cacheData = cacheFile.Content.ToStringUtf8().Split("\u1145");
if (Utils.GetGameHash(cacheData[0]) != uint.Parse(cacheData[1])) {
return false;
}
path = cacheData[0];
return true;
} catch (Exception) {
return false;
}
}
static bool TryReadGamePathFromUnityLog([NotNullWhen(true)] out string? path) {
path = null;
try {
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var logPath = ProductNames
.Select(name => $"{appDataPath}/../LocalLow/miHoYo/{name}/output_log.txt")
.Where(File.Exists)
.MaxBy(File.GetLastWriteTime);
if (logPath == null) {
return false;
}
return (path = GetGamePathFromLogFile(logPath) ?? GetGamePathFromLogFile($"{logPath}.last")) != null;
} catch (Exception) {
return false;
}
}
static string ReadGamePathFromProcess() {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).Start(App.ConfigNeedStartGenshin, _ => {
Process? proc;
while ((proc = Utils.GetGameProcess()) == null) {
Thread.Sleep(250);
}
var fileName = proc.GetFileName()!;
proc.Kill();
return fileName;
});
}
}
private static string? GetGamePathFromLogFile(string path) {
if (!File.Exists(path)) {
return null;
}
var content = File.ReadAllText(path);
var matchResult = GamePathRegex().Match(content);
if (!matchResult.Success) {
return null;
}
var entryName = matchResult.Groups["1"].Value.Replace("_Data", ".exe");
var fullPath = Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
return File.Exists(fullPath) ? fullPath : null;
}
[GeneratedRegex(@"(?m).:(?:\\|/).+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
private static partial Regex GamePathRegex();
}

View File

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

View File

@@ -0,0 +1,35 @@
global using System.Diagnostics;
global using YaeAchievement.res;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Proto;
namespace YaeAchievement;
public static class GlobalVars {
public static bool PauseOnExit { get; set; } = true;
public static Version AppVersion { get; } = Assembly.GetEntryAssembly()!.GetName().Version!;
public static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory;
private static readonly string CommonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
public static readonly string DataPath = Path.Combine(CommonData, "Yae");
public static readonly string CachePath = Path.Combine(DataPath, "cache");
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
public const uint AppVersionCode = 243;
public const string AppVersionName = "5.7.3";
public const string PipeName = "YaeAchievementPipe";
[field:MaybeNull]
public static AchievementInfo AchievementInfo =>
field ??= AchievementInfo.Parser.ParseFrom(Utils.GetBucketFile("schicksal/metadata").GetAwaiter().GetResult());
static GlobalVars() {
Directory.CreateDirectory(DataPath);
Directory.CreateDirectory(CachePath);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
using System.ComponentModel;
namespace System.Collections.Generic {
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class CollectionExtensions {
public static IDictionary<TKey, TValue> RemoveValues<TKey, TValue>(
this IDictionary<TKey, TValue> dictionary, params TKey[] keys
) {
foreach (var key in keys) {
dictionary.Remove(key);
}
return dictionary;
}
}
}
namespace System.Linq {
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class EnumerableExtensions {
public static IEnumerable<IGrouping<TKey, TKey>> GroupKeys<TKey, TValue>(
this IEnumerable<Dictionary<TKey, TValue>> source,
Func<TValue, bool> condition
) where TKey : notnull => source
.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key))
.GroupBy(x => x);
}
}

View File

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

View File

@@ -0,0 +1,38 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Spectre.Console;
// ReSharper disable CheckNamespace
namespace Google.Protobuf;
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class BinaryReaderExtensions {
public static byte[] ReadBytes(this BinaryReader reader) {
try {
var length = reader.ReadInt32();
if (length is < 0 or > 114514 * 2) {
throw new ArgumentException(nameof(length));
}
return reader.ReadBytes(length);
} catch (Exception e) when (e is IOException or ArgumentException) {
AnsiConsole.WriteLine(App.StreamReadDataFail);
Environment.Exit(-1);
throw new UnreachableException();
}
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class CodedInputStreamExtensions {
[UnsafeAccessor(UnsafeAccessorKind.Method)]
private static extern byte[] ReadRawBytes(CodedInputStream stream, int size);
public static CodedInputStream ReadLengthDelimitedAsStream(this CodedInputStream stream) {
return new CodedInputStream(ReadRawBytes(stream, stream.ReadLength()));
}
}

View File

@@ -0,0 +1,127 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using Windows.Win32.System.LibraryLoader;
using Windows.Win32.System.Threading;
using Spectre.Console;
using static Windows.Win32.System.Memory.VIRTUAL_ALLOCATION_TYPE;
using static Windows.Win32.System.Memory.PAGE_PROTECTION_FLAGS;
using static Windows.Win32.System.Memory.VIRTUAL_FREE_TYPE;
// ReSharper disable MemberCanBePrivate.Global
namespace YaeAchievement.Utilities;
internal sealed unsafe class GameProcess {
public uint Id { get; }
public HANDLE Handle { get; }
public HANDLE MainThreadHandle { get; }
public event Action? OnExit;
public GameProcess(string path) {
const PROCESS_CREATION_FLAGS flags = PROCESS_CREATION_FLAGS.CREATE_SUSPENDED;
Span<char> cmdLines = stackalloc char[1]; // "\0"
var si = new STARTUPINFOW {
cb = (uint) sizeof(STARTUPINFOW)
};
var wd = Path.GetDirectoryName(path)!;
if (!Native.CreateProcess(path, ref cmdLines, null, null, false, flags, null, wd, si, out var pi)) {
var argumentData = new Dictionary<string, object> {
{ "path", path },
{ "workdir", wd },
{ "file_exists", File.Exists(path) },
};
throw new ApplicationException($"CreateProcess fail: {Marshal.GetLastPInvokeErrorMessage()}") {
Data = {
{ "sentry:context:Arguments", argumentData }
}
};
}
Id = pi.dwProcessId;
Handle = pi.hProcess;
MainThreadHandle = pi.hThread;
Task.Run(() => {
Native.WaitForSingleObject(Handle, 0xFFFFFFFF); // INFINITE
OnExit?.Invoke();
}).ContinueWith(task => { if (task.IsFaulted) Utils.OnUnhandledException(task.Exception!); });
}
public void LoadLibrary(string libPath) {
try {
var hKrnl32 = NativeLibrary.Load("kernel32");
var mLoadLibraryW = NativeLibrary.GetExport(hKrnl32, "LoadLibraryW");
var libPathLen = (uint) libPath.Length * sizeof(char);
var lpLibPath = Native.VirtualAllocEx(Handle, null, libPathLen + 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (lpLibPath == null) {
throw new Win32Exception { Data = { { "api", "VirtualAllocEx" } } };
}
fixed (void* lpBuffer = libPath) {
if (!Native.WriteProcessMemory(Handle, lpLibPath, lpBuffer, libPathLen)) {
throw new Win32Exception { Data = { { "api", "WriteProcessMemory" } } };
}
}
var lpStartAddress = (delegate*unmanaged[Stdcall]<void*, uint>) mLoadLibraryW; // THREAD_START_ROUTINE
var hThread = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress, lpLibPath, 0);
if (hThread.IsNull) {
throw new Win32Exception { Data = { { "api", "CreateRemoteThread" } } };
}
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
}
// Get lib base address in target process
byte* baseAddress = null;
using (var hSnap = Native.CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, Id)) {
if (hSnap.IsInvalid) {
throw new Win32Exception { Data = { { "api", "CreateToolhelp32Snapshot" } } };
}
var moduleEntry = new MODULEENTRY32 {
dwSize = (uint) sizeof(MODULEENTRY32)
};
if (Native.Module32First(hSnap, ref moduleEntry)) {
do {
if (new string((sbyte*) &moduleEntry.szExePath._0) == libPath) {
baseAddress = moduleEntry.modBaseAddr;
break;
}
} while (Native.Module32Next(hSnap, ref moduleEntry));
}
}
if (baseAddress == null) {
throw new InvalidOperationException("No matching module found in target process.");
}
//
using var libHandle = Native.LoadLibraryEx(libPath, LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES);
if (libHandle.IsInvalid) {
throw new Win32Exception { Data = { { "api", "LoadLibraryEx" } } };
}
var libMainProc = Native.GetProcAddress(libHandle, "YaeMain");
if (libMainProc.IsNull) {
throw new Win32Exception { Data = { { "api", "GetProcAddress" } } };
}
var libMainProcRVA = libMainProc.Value - libHandle.DangerousGetHandle();
var lpStartAddress2 = (delegate*unmanaged[Stdcall]<void*, uint>) (baseAddress + libMainProcRVA); // THREAD_START_ROUTINE
//
var hThread2 = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress2, null, 0);
if (hThread2.IsNull) {
throw new Win32Exception { Data = { { "api", "CreateRemoteThread2" } } };
}
Native.CloseHandle(hThread2);
Native.CloseHandle(hThread);
} catch (Win32Exception e) {
_ = Terminate(0);
AnsiConsole.WriteLine(App.LoadLibraryFail, e.Data["api"]!, e.NativeErrorCode, e.Message);
Environment.Exit(-1);
}
}
public bool ResumeMainThread() => Native.ResumeThread(MainThreadHandle) != 0xFFFFFFFF;
public bool Terminate(uint exitCode) => Native.TerminateProcess(Handle, exitCode);
}

View File

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

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

@@ -0,0 +1,358 @@
using System.ComponentModel;
using System.Globalization;
using System.IO.Compression;
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using Proto;
using Spectre.Console;
using YaeAchievement.Utilities;
namespace YaeAchievement;
public static class Utils {
public static HttpClient CHttpClient { get; } = new (new SentryHttpMessageHandler(new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
})) {
DefaultRequestHeaders = {
UserAgent = {
new ProductInfoHeaderValue("YaeAchievement", GlobalVars.AppVersion.ToString(2))
}
}
};
public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
SentrySdk.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction);
var cacheKey = useCache ? path : null;
//
if (_updateInfo?.CdnFiles.TryGetValue(path, out var cdnFile) == true) {
try {
var data = await cdnFile.Urls
.Select(url => GetFileFromCdn(url, cacheKey, cdnFile.Hash, cdnFile.Size))
.WhenFirstSuccessful()
.Unwrap();
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException or InvalidDataException) {}
}
//
try {
var data = await WhenFirstSuccessful([
GetFileReal($"https://rin.holohat.work/{path}", cacheKey),
GetFileReal($"https://ena-rin.holohat.work//{path}", cacheKey),
GetFileReal($"https://cn-cd-1259389942.file.myqcloud.com/{path}", cacheKey)
]).Unwrap();
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
AnsiConsole.WriteLine(App.NetworkError, e.Message);
}
transaction.Finish();
Environment.Exit(-1);
throw new UnreachableException();
static async Task<byte[]> GetFileFromCdn(string url, string? cacheKey, uint hash, uint size) {
var data = await GetFileReal(url, cacheKey);
if (data.Length != size || Crc32.Compute(data) != hash) {
throw new InvalidDataException();
}
if (data.Length > 44 && Unsafe.As<byte, uint>(ref data[0]) == 0x38464947) { // GIF8
var seed = Unsafe.As<byte, uint>(ref data[44]) ^ 0x01919810;
var hush = Unsafe.As<byte, uint>(ref data[48]) - 0x32123432; //          
var span = data.AsSpan()[52..];
Span<byte> xorTable = stackalloc byte[4096];
new Random((int) seed).NextBytes(xorTable);
for (var i = 0; i < span.Length; i++) {
span[i] ^= xorTable[i % 4096];
}
using var dataStream = new MemoryStream();
unsafe {
fixed (byte* p = span) {
var cmpStream = new UnmanagedMemoryStream(p, span.Length);
using var decompressor = new BrotliStream(cmpStream, CompressionMode.Decompress);
// ReSharper disable once MethodHasAsyncOverload
decompressor.CopyTo(dataStream);
}
}
data = dataStream.ToArray();
if (Crc32.Compute(data) != hush) {
throw new InvalidDataException();
}
}
return data;
}
static async Task<byte[]> GetFileReal(string url, string? cacheKey) {
using var reqwest = new HttpRequestMessage(HttpMethod.Get, url);
CacheItem? cache = null;
if (cacheKey != null && CacheFile.TryRead(cacheKey, out cache)) {
reqwest.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
}
using var response = await CHttpClient.SendAsync(reqwest);
if (cache != null && response.StatusCode == HttpStatusCode.NotModified) {
return cache.Content.ToByteArray();
}
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
if (cacheKey != null) {
var etag = response.Headers.ETag!.Tag;
CacheFile.Write(cacheKey, bytes, etag);
}
return bytes;
}
}
public static int ToIntOrDefault(string? value, int defaultValue = 0) {
return value != null && int.TryParse(value, out var result) ? result : defaultValue;
}
public static bool ToBooleanOrDefault(string? value, bool defaultValue = false) {
return value != null && bool.TryParse(value, out var result) ? result : defaultValue;
}
public static unsafe void CopyToClipboard(string text) {
if (Native.OpenClipboard(HWND.Null)) {
Native.EmptyClipboard();
var hGlobal = (HGLOBAL) Marshal.AllocHGlobal((text.Length + 1) * 2);
var hPtr = (nint) Native.GlobalLock(hGlobal);
Marshal.Copy(text.ToCharArray(), 0, hPtr, text.Length);
Native.GlobalUnlock((HGLOBAL) hPtr);
Native.SetClipboardData(13, new HANDLE(hPtr));
Marshal.FreeHGlobal(hGlobal);
Native.CloseClipboard();
} else {
throw new Win32Exception();
}
}
// ReSharper disable once NotAccessedField.Local
private static UpdateInfo? _updateInfo;
public static Task StartSpinnerAsync(string status, Func<StatusContext, Task> func) {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
}
public static Task<T> StartSpinnerAsync<T>(string status, Func<StatusContext, Task<T>> func) {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
}
public static async Task CheckUpdate(bool useLocalLib) {
try {
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
var versionInfo = _updateInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
if (versionInfo.EnableAutoUpdate) {
var newBin = await StartSpinnerAsync(App.UpdateDownloading, _ => GetBucketFile(versionInfo.PackageLink));
var tmpPath = Path.GetTempFileName();
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
await using (var dstStream = File.Open($"{GlobalVars.DataPath}/update.exe", FileMode.Create)) {
await using var srcStream = typeof(Program).Assembly.GetManifestResourceStream("updater")!;
await srcStream.CopyToAsync(dstStream);
}
await File.WriteAllBytesAsync(tmpPath, newBin);
ShellOpen(updaterPath, $"{Environment.ProcessId} \"{tmpPath}\"");
await StartSpinnerAsync(App.UpdateChecking, _ => Task.Delay(1919810));
GlobalVars.PauseOnExit = false;
Environment.Exit(0);
}
AnsiConsole.MarkupLine($"[link]{App.DownloadLink}[/]", versionInfo.PackageLink);
if (versionInfo.ForceUpdate) {
Environment.Exit(0);
}
}
if (versionInfo.EnableLibDownload && !useLocalLib) {
var data = await GetBucketFile("schicksal/lic.dll");
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data); // 要求重启电脑
}
} catch (IOException e) when ((uint) e.HResult == 0x80070020) { // ERROR_SHARING_VIOLATION
// IO_SharingViolation_File
// The process cannot access the file '{0}' because it is being used by another process.
AnsiConsole.WriteLine("文件 {0} 被其它程序占用,请 重启电脑 或 解除文件占用 后重试。", e.Message[36..^46]);
Environment.Exit(-1);
}
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static bool ShellOpen(string path, string args = "") {
try {
return new Process {
StartInfo = new ProcessStartInfo {
FileName = path,
UseShellExecute = true,
Arguments = args
}
}.Start();
} catch (Exception) {
return false;
}
}
internal static Process? GetGameProcess() => Process.GetProcessesByName("YuanShen")
.Concat(Process.GetProcessesByName("GenshinImpact"))
.FirstOrDefault(p => File.Exists($"{p.GetFileName()}/../HoYoKProtect.sys"));
private static GameProcess? _proc;
public static void InstallExitHook() {
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
_proc?.Terminate(0);
if (GlobalVars.PauseOnExit) {
AnsiConsole.WriteLine(App.PressKeyToExit);
Console.ReadKey();
}
};
}
public static void InstallExceptionHook() {
AppDomain.CurrentDomain.UnhandledException += (_, e) => OnUnhandledException((Exception) e.ExceptionObject);
}
public static void OnUnhandledException(Exception ex) {
SentrySdk.CaptureException(ex);
switch (ex) {
case ApplicationException ex1:
AnsiConsole.WriteLine(ex1.Message);
break;
case SocketException ex2:
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
break;
case HttpRequestException ex3:
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
break;
default:
AnsiConsole.WriteLine(ex.ToString());
break;
}
Environment.Exit(-1);
}
private static bool _isUnexpectedExit = true;
// ReSharper disable once UnusedMethodReturnValue.Global
public static void StartAndWaitResult(string exePath, Dictionary<int, Func<BinaryReader, bool>> handlers, Action onFinish) {
var hash = GetGameHash(exePath);
var nativeConf = GlobalVars.AchievementInfo.NativeConfig;
if (!nativeConf.MethodRva.TryGetValue(hash, out var methodRva)) {
AnsiConsole.WriteLine($"No match config {exePath} {hash:X8}");
Environment.Exit(0);
return;
}
Task.Run(() => {
try {
using var stream = new NamedPipeServerStream(GlobalVars.PipeName);
using var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, true);
using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true);
stream.WaitForConnection();
int type;
while ((type = stream.ReadByte()) != -1) {
switch (type) {
case 0xFC:
writer.Write(nativeConf.AchievementCmdId);
writer.Write(nativeConf.StoreCmdId);
break;
case 0xFD:
writer.Write(methodRva.DoCmd);
writer.Write(methodRva.UpdateNormalProp);
writer.Write(methodRva.NewString);
writer.Write(methodRva.FindGameObject);
writer.Write(methodRva.EventSystemUpdate);
writer.Write(methodRva.SimulatePointerClick);
writer.Write(methodRva.ToInt32);
writer.Write(methodRva.TcpStatePtr);
writer.Write(methodRva.SharedInfoPtr);
writer.Write(methodRva.Decompress);
break;
case 0xFE:
_proc!.ResumeMainThread();
break;
case 0xFF:
writer.Write(true);
_isUnexpectedExit = false;
onFinish();
return;
}
if (handlers.TryGetValue(type, out var handler)) {
if (handler(reader)) {
handlers.Remove(type);
}
}
}
} catch (IOException e) when (e.Message == "Pipe is broken.") { } // SR.IO_PipeBroken
}).ContinueWith(task => { if (task.IsFaulted) OnUnhandledException(task.Exception!); });
_proc = new GameProcess(exePath);
_proc.LoadLibrary(GlobalVars.LibFilePath);
_proc.OnExit += () => {
if (_isUnexpectedExit) {
_proc = null;
AnsiConsole.WriteLine(App.GameProcessExit);
Environment.Exit(114514);
}
};
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
}
public static uint GetGameHash(string exePath) {
try {
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(exePath);
_ = stream.ReadAtLeast(buffer, 0x10000, false);
return Crc32.Compute(buffer);
} catch (IOException) {
return 0xFFFFFFFF;
}
}
internal static unsafe void SetQuickEditMode(bool enable) {
var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
CONSOLE_MODE mode = default;
Native.GetConsoleMode(handle, &mode);
mode = enable ? mode | CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE : mode &~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE;
Native.SetConsoleMode(handle, mode);
}
internal static unsafe void FixTerminalFont() {
if (!CultureInfo.CurrentCulture.Name.StartsWith("zh")) {
return;
}
var handle = Native.GetStdHandle(STD_HANDLE.STD_OUTPUT_HANDLE);
var fontInfo = new CONSOLE_FONT_INFOEX {
cbSize = (uint) sizeof(CONSOLE_FONT_INFOEX)
};
if (!Native.GetCurrentConsoleFontEx(handle, false, &fontInfo)) {
return;
}
if (fontInfo.FaceName.ToString() == "Terminal") { // 点阵字体
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); // todo: use better way like auto set console font etc.
}
}
// https://stackoverflow.com/a/76953892
private static async Task<Task<TResult>> WhenFirstSuccessful<TResult>(this IEnumerable<Task<TResult>> tasks) {
var cts = new CancellationTokenSource();
Task<TResult>? selectedTask = null;
var continuations = tasks
.TakeWhile(_ => !cts.IsCancellationRequested)
.Select(task => {
return task.ContinueWith(t => {
if (t.IsCompletedSuccessfully) {
if (Interlocked.CompareExchange(ref selectedTask, t, null) is null) {
cts.Cancel();
}
}
}, cts.Token, TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
});
var whenAll = Task.WhenAll(continuations);
try {
await whenAll.ConfigureAwait(false);
} catch when (whenAll.IsCanceled) { /* ignore */ }
return selectedTask!;
}
}

View File

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

Binary file not shown.

View File

@@ -0,0 +1,217 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Yae.Utilities;
using static Yae.GameMethod;
namespace Yae;
internal static unsafe class Application {
private static bool _initialized;
[UnmanagedCallersOnly(EntryPoint = "YaeMain")]
private static uint Awake(nint hModule) {
if (Interlocked.Exchange(ref _initialized, true)) {
return 1;
}
Native.RegisterUnhandledExceptionHandler();
Log.UseConsoleOutput();
Log.Trace("~");
Goshujin.Init();
Goshujin.LoadCmdTable();
Goshujin.LoadMethodTable();
Goshujin.ResumeMainThread();
//
Native.WaitMainWindow();
Log.ResetConsole();
//
RecordChecksum();
MinHook.Attach(DoCmd, &OnDoCmd, out _doCmd);
MinHook.Attach(ToUInt32, &OnToInt32, out _toInt32);
MinHook.Attach(UpdateNormalProp, &OnUpdateNormalProp, out _updateNormalProp);
MinHook.Attach(EventSystemUpdate, &OnEventSystemUpdate, out _eventSystemUpdate);
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "YaeWndHook")]
private static nint WndHook(int nCode, nint wParam, nint lParam) {
((delegate*unmanaged<nint, uint>) &Awake)(0);
return User32.CallNextHookEx(0, nCode, wParam, lParam);
}
#region RecvPacket
private static delegate*unmanaged<byte*, int, int> _toInt32;
[UnmanagedCallersOnly]
private static int OnToInt32(byte* val, int startIndex) {
var ret = _toInt32(val, startIndex);
if (startIndex != 6 || *(ushort*) (val += 0x20) != 0x6745) {
return ret;
}
var cmdId = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 2));
if (cmdId == CmdId.PlayerStoreNotify) {
Goshujin.PushStoreData(GetData(val));
} else if (cmdId == CmdId.AchievementAllDataNotify) {
Goshujin.PushAchievementData(GetData(val));
}
return ret;
static Span<byte> GetData(byte* val) {
var headLen = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 4));
var headPtr = val + 10;
var dataLen = BinaryPrimitives.ReverseEndianness(*(uint*) (val + 6));
var dataPtr = val + 10 + headLen;
var unzipLen = GetDecompressedSize(new Span<byte>(headPtr, headLen));
if (unzipLen == 0) {
return new Span<byte>(dataPtr, (int) dataLen);
}
var unzipBuf = NativeMemory.Alloc(unzipLen);
if (!Decompress(*TcpStatePtr, *SharedInfoPtr, dataPtr, dataLen, unzipBuf, unzipLen)) {
throw new InvalidDataException("Decompress failed.");
}
return new Span<byte>(unzipBuf, (int) unzipLen);
}
}
private static uint GetDecompressedSize(Span<byte> header) {
var offset = 0;
ulong tag;
while (offset != header.Length && (tag = ReadRawVarInt64(header, ref offset)) != 0) {
if (tag == 64) {
return (uint) ReadRawVarInt64(header, ref offset);
}
switch (tag & 7) {
case 0:
ReadRawVarInt64(header, ref offset);
break;
case 1:
offset += 8;
break;
case 2:
offset += (int) ReadRawVarInt64(header, ref offset);
break;
case 3:
case 4:
throw new NotSupportedException();
case 5:
offset += 4;
break;
}
}
return 0;
}
private static ulong ReadRawVarInt64(this Span<byte> span, ref int offset) {
ulong result = 0;
for (var i = 0; i < 8; i++) {
var b = span[offset++];
result |= (ulong) (b & 0x7F) << (i * 7);
if (b < 0x80) {
return result;
}
}
throw new InvalidDataException("CodedInputStream encountered a malformed varint.");
}
#endregion
#region Prop
/*
* PROP_PLAYER_HCOIN = 10015,
* PROP_PLAYER_WAIT_SUB_HCOIN = 10022,
* PROP_PLAYER_SCOIN = 10016,
* PROP_PLAYER_WAIT_SUB_SCOIN = 10023,
* PROP_PLAYER_MCOIN = 10025,
* PROP_PLAYER_WAIT_SUB_MCOIN = 10026,
* PROP_PLAYER_HOME_COIN = 10042,
* PROP_PLAYER_WAIT_SUB_HOME_COIN = 10043,
* PROP_PLAYER_ROLE_COMBAT_COIN = 10053,
* PROP_PLAYER_MUSIC_GAME_BOOK_COIN = 10058,
*/
public static HashSet<int> RequiredPlayerProperties { get; } = [
10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058
];
private static delegate*unmanaged<nint, int, double, double, int, void> _updateNormalProp;
[UnmanagedCallersOnly]
private static void OnUpdateNormalProp(nint @this, int type, double value, double lastValue, int state) {
_updateNormalProp(@this, type, value, lastValue, state);
if (RequiredPlayerProperties.Remove(type)) {
Goshujin.PushPlayerProp(type, value);
}
}
#endregion
#region Checksum
[StructLayout(LayoutKind.Sequential)]
private struct RecordChecksumCmdData {
public int Type;
public void* Buffer;
public int Length;
}
private static readonly RecordChecksumCmdData[] RecordedChecksum = new RecordChecksumCmdData[3];
private static void RecordChecksum() {
for (var i = 0; i < 3; i++) {
var buffer = NativeMemory.AllocZeroed(256);
var data = new RecordChecksumCmdData {
Type = i,
Buffer = buffer,
Length = 256
};
_ = DoCmd(23, Unsafe.AsPointer(ref data), sizeof(RecordChecksumCmdData));
RecordedChecksum[i] = data;
//REPL//Log.Trace($"nType={i}, value={new string((sbyte*) buffer, 0, data.Length)}");
}
}
private static delegate*unmanaged<int, void*, int, int> _doCmd;
[UnmanagedCallersOnly]
public static int OnDoCmd(int cmdType, void* data, int size) {
var result = _doCmd(cmdType, data, size);
if (cmdType == 23) {
var cmdData = (RecordChecksumCmdData*) data;
if (cmdData->Type < 3) {
var recordedData = RecordedChecksum[cmdData->Type];
cmdData->Length = recordedData.Length;
Buffer.MemoryCopy(recordedData.Buffer, cmdData->Buffer, recordedData.Length, recordedData.Length);
//REPL//Log.Trace($"Override type {cmdData->Type} result");
}
}
return result;
}
#endregion
#region EnterGate
private static long _lastTryEnterTime;
private static delegate*unmanaged<nint, void> _eventSystemUpdate;
[UnmanagedCallersOnly]
public static void OnEventSystemUpdate(nint @this) {
_eventSystemUpdate(@this);
if (Environment.TickCount64 - _lastTryEnterTime > 200) {
var obj = FindGameObject(NewString("BtnStart"u8.AsPointer()));
if (obj != 0 && SimulatePointerClick(@this, obj)) {
MinHook.Detach((nint) EventSystemUpdate);
}
_lastTryEnterTime = Environment.TickCount64;
}
}
#endregion
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

180
app.js
View File

@@ -1,180 +0,0 @@
const proxy = require("udp-proxy")
const cp = require("child_process")
const rs = require("./regionServer")
const appcenter = require("./appcenter")
const {
initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkCDN, checkUpdate,
brotliCompressSync, brotliDecompressSync, checkGameIsRunning, checkPortIsUsing
} = require("./utils")
const { exportData } = require("./export")
const { enablePrivilege } = require("./native")
const onExit = () => {
setupHost(true)
console.log("按任意键退出")
cp.execSync("pause > nul", { stdio: "inherit" })
};
(async () => {
try {
process.once("SIGHUP", () => setupHost(true))
process.on("unhandledRejection", (reason, promise) => {
console.log("Unhandled Rejection at: ", promise, "\n0Reason:", reason)
})
process.once("uncaughtException", (err, origin) => {
appcenter.uploadError(err, true)
console.log(err)
console.log(`Origin: ${origin}`)
process.exit(1)
})
process.once("exit", onExit)
process.once("SIGINT", onExit)
try {
enablePrivilege()
} catch (e) {
console.log("请使用管理员身份运行此程序")
process.exit(-1)
}
appcenter.startup()
let conf = await initConfig()
checkPortIsUsing()
checkGameIsRunning()
log("检查更新")
await checkUpdate()
checkCDN().then(_ => debug("CDN check success.")).catch(reason => {
console.log(reason)
process.exit(113)
})
let gameProcess
let unexpectedExit = true
rs.create(conf,() => {
setupHost()
gameProcess = cp.execFile(conf.executable, { cwd: conf.path },err => {
if (err !== null && !err.killed) {
throw err
}
})
log("启动原神")
gameProcess.on("exit", () => {
if (unexpectedExit) {
console.log("游戏进程异常退出")
process.exit(0)
}
})
},(ip, port, hServer) => {
let login = false
let cache = new Map()
let lastRecvTimestamp = 0
// noinspection JSUnusedGlobalSymbols
const options = {
address: ip,
port: port,
localaddress: "127.0.0.1",
localport: 45678,
middleware: {
message: (msg, sender, next) => {
const buf = Buffer.from(msg)
if (!(login && buf.readUInt8(8) === 0x51)) {
next(msg, sender)
}
},
proxyMsg: (msg, sender, peer, next) => {
try { next(msg, sender, peer) } catch (e) {}
}
}
}
let monitor;
let stopped = false
const createMonitor = () => {
monitor = setInterval(async () => {
if (login && lastRecvTimestamp + 2 < parseInt(Date.now() / 1000) && !stopped) {
stopped = true
unexpectedExit = false
server.close(() => {})
hServer.close()
gameProcess.kill()
clearInterval(monitor)
setupHost(true)
log("正在处理数据,请稍后...")
let packets = Array.from(cache.values())
cache.clear()
packets.sort((a, b) => a.frg - b.frg)
.sort((a, b) => a.sn - b.sn)
.filter(i => i.data.byteLength !== 0)
.forEach(i => {
const psn = i.sn + i.frg
cache.has(psn) ? (() => {
const arr = cache.get(psn)
arr.push(i.data)
cache.set(psn, arr)
})() : cache.set(psn, [i.data])
})
packets = Array.from(cache.values())
.map(arr => {
const data = Buffer.concat(arr)
const len = Buffer.alloc(4)
len.writeUInt32LE(data.length)
return Buffer.concat([len, data])
})
const merged = Buffer.concat(packets)
const compressed = brotliCompressSync(merged)
const response = await upload(compressed)
const data = brotliDecompressSync(response.data)
if (response.status !== 200) {
log(`发生错误: ${data}`)
log(`请求ID: ${response.headers["x-api-requestid"]}`)
log("请联系开发者以获取帮助")
} else {
const proto = await decodeProto(data,"AllAchievement")
await exportData(proto)
}
process.exit(0)
}
},1000)
}
const server = proxy.createServer(options)
server.on("message", (msg, _) => {
if (msg.byteLength > 500) {
login = true
}
})
server.on("error", err => console.log(`Proxy error: ${err.message}` + err.message))
server.on("proxyError", err => console.log(`Proxy error: ${err.message}` + err.message))
server.on("proxyMsg", (msg, _) => {
lastRecvTimestamp = parseInt(Date.now() / 1000)
let buf = Buffer.from(msg)
if (buf.byteLength <= 20) {
switch(buf.readUInt32BE(0)) {
case 325:
createMonitor()
debug("服务端握手应答")
break
case 404:
lastRecvTimestamp = parseInt(Date.now() / 1000) - 2333
break
default:
console.log(`Unhandled: ${buf.toString("hex")}`)
process.exit(2)
break
}
return
}
splitPacket(buf).forEach(sb => {
if (sb.readUInt8(8) === 0x51) {
const p = new KPacket(sb)
if (!cache.has(p.hash)) cache.set(p.hash, p)
}
})
})
return server
}).then(() => log("加载完毕"))
} catch (e) {
console.log(e)
if (e instanceof Error) {
appcenter.uploadError(e, true)
} else {
appcenter.uploadError(Error(e), true)
}
process.exit(1)
}
})()

View File

@@ -1,101 +0,0 @@
const axios = require("axios")
const crypto = require("crypto")
const { version } = require("./version")
const { getDeviceID, getDeviceInfo } = require("./native")
const getTimestamp = (d = new Date()) => {
const p = i => i.toString().padStart(2, "0")
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}.${p(d.getUTCMilliseconds())}Z`
}
const queue = []
const session = crypto.randomUUID()
const key = "648b83bf-d439-49bd-97f4-e1e506bdfe39"
const install = (() => {
const id = getDeviceID()
return id === undefined ? crypto.randomUUID() : id
})()
const device = (() => {
const info = getDeviceInfo()
info.appBuild = version.code
info.appVersion = version.name
info.sdkName = "appcenter.wpf.netcore"
info.sdkVersion = "4.5.0"
info.osName = "WINDOWS"
info.appNamespace = "default"
return info
})()
const upload = () => {
if (queue.length > 0) {
const data = JSON.stringify({ "logs": queue })
axios.post("https://in.appcenter.ms/logs?api-version=1.0.0", data,{
headers: {
"App-Secret": key,
"Install-ID": install
}
}).then(_ => {
queue.length = 0
}).catch(_ => {})
}
}
const uploadError = (err, fatal) => {
const eid = crypto.randomUUID()
const reportJson = process.report.getReport(err)
const reportAttachment = {
type: "errorAttachment",
device: device,
timestamp: getTimestamp(),
id: crypto.randomUUID(),
sid: session,
errorId: eid,
contentType: "application/json",
fileName: "report.json",
data: Buffer.from(JSON.stringify(reportJson, null, 2), "utf-8").toString("base64")
}
// noinspection JSUnresolvedVariable
const errorContent = {
type: "managedError",
id: eid,
sid: session,
architecture: "AMD64",
userId: install,
fatal: fatal,
processId: process.pid,
processName: process.argv0.replaceAll("\\", "/").split("/").pop(),
timestamp: getTimestamp(),
appLaunchTimestamp: getTimestamp(new Date(Date.now() - process.uptime())),
exception: {
"type": err.name,
"message": err.message,
"stackTrace": err.stack
},
device: device
}
queue.push(errorContent, reportAttachment)
upload()
}
const startup = () => {
queue.push({
type: "startService",
services: [ "Analytics","Crashes" ],
timestamp: getTimestamp(),
device: device
})
queue.push({
type: "startSession",
sid: session,
timestamp: getTimestamp(),
device: device
})
upload()
setInterval(() => upload(), 10000)
}
module.exports = {
startup, upload, uploadError
}

141
export.js
View File

@@ -1,141 +0,0 @@
const fs = require("fs")
const readline = require("readline")
const { loadCache, log } = require("./utils")
const { copyToClipboard } = require("./native")
const exportToSeelie = proto => {
const out = { achievements: {} }
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
out.achievements[id] = { done: true }
})
const fp = `./export-${Date.now()}-seelie.json`
fs.writeFileSync(fp, JSON.stringify(out))
log(`导出为文件: ${fp}`)
}
const exportToPaimon = async proto => {
const out = { achievement: {} }
const achTable = new Map()
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
excel.forEach(({GoalId, Id}) => {
achTable.set(Id, GoalId === undefined ? 0 : GoalId)
})
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
const gid = achTable.get(id)
if (out.achievement[gid] === undefined) {
out.achievement[gid] = {}
}
out.achievement[gid][id] = true
})
const fp = `./export-${Date.now()}-paimon.json`
fs.writeFileSync(fp, JSON.stringify(out))
log(`导出为文件: ${fp}`)
}
const exportToCocogoat = async proto => {
const out = {
value: {
achievements: []
}
}
const achTable = new Map()
const preStageAchievementIdList = []
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
excel.forEach(({GoalId, Id, PreStageAchievementId}) => {
if (PreStageAchievementId !== undefined) {
preStageAchievementIdList.push(PreStageAchievementId)
}
achTable.set(Id, GoalId === undefined ? 0 : GoalId)
})
const p = i => i.toString().padStart(2, "0")
const getDate = ts => {
const d = new Date(parseInt(`${ts}000`))
return `${d.getFullYear()}/${p(d.getMonth()+1)}/${p(d.getDate())}`
}
proto.list.filter(achievement => achievement.status === 3).forEach(({current, finishTimestamp, id, require}) => {
out.value.achievements.push({
id: id,
status: current === undefined || current === 0 || preStageAchievementIdList.includes(id) ? `${require}/${require}` : `${current}/${require}`,
categoryId: achTable.get(id),
date: getDate(finishTimestamp)
})
})
const ts = Date.now()
const json = JSON.stringify(out,null,2)
copyToClipboard(json)
const fp = `./export-${ts}-cocogoat.json`
fs.writeFileSync(fp, json)
log(`导出内容已复制到剪贴板`)
}
const exportToCsv = async proto => {
const excel = await loadCache("achievement-data.json", "HolographicHat/genshin-achievement-export")
const achievementMap = new Map()
excel["achievement"].forEach(obj => {
achievementMap.set(parseInt(obj.id), obj)
})
const outputLines = ["ID,状态,特辑,名称,描述,当前进度,目标进度,完成时间"]
const getStatusText = i => {
switch (i) {
case 1: return "未完成"
case 2: return "已完成,未领取奖励"
case 3: return "已完成"
default: return "未知"
}
}
const getTime = ts => {
const d = new Date(parseInt(`${ts}000`))
const p = i => i.toString().padStart(2, "0")
return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
const bl = [84517]
proto.list.forEach(({current, finishTimestamp, id, status, require}) => {
if (!bl.includes(id)) {
const desc = achievementMap.get(id) === undefined ? (() => {
console.log(`Error get id ${id} in excel`)
return {
goal: "未知",
name: "未知",
desc: "未知"
}
})() : achievementMap.get(id)
outputLines.push(`${id},${getStatusText(status)},${excel.goal[desc.goal]},${desc.name},${desc.desc},${status !== 1 ? current === 0 ? require : current : current},${require},${status === 1 ? "" : getTime(finishTimestamp)}`)
}
})
const fp = `./export-${Date.now()}.csv`
fs.writeFileSync(fp, `\uFEFF${outputLines.join("\n")}`)
log(`导出为文件: ${fp}`)
}
const exportData = async proto => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const question = (query) => new Promise(resolve => {
rl.question(query, resolve)
})
const chosen = await question("导出至: \n[0] 椰羊 (https://cocogoat.work/achievement)\n[1] Paimon.moe\n[2] Seelie.me\n[3] 表格文件 (默认)\n输入一个数字(0-3): ")
rl.close()
switch (chosen) {
case "0":
await exportToCocogoat(proto)
break
case "1":
await exportToPaimon(proto)
break
case "2":
await exportToSeelie(proto)
break
case "raw":
fs.writeFileSync(`./export-${Date.now()}-raw.json`, JSON.stringify(proto,null,2))
log("OK")
break
default:
await exportToCsv(proto)
}
}
module.exports = {
exportData
}

7
native.d.ts vendored
View File

@@ -1,7 +0,0 @@
export function selectGameExecutable(): string
export function checkGameIsRunning(processName: string): boolean
export function whoUseThePort(port: number): object
export function copyToClipboard(value: string): any
export function enablePrivilege(): any
export function getDeviceInfo(): any
export function getDeviceID(): string

5
native/.gitignore vendored
View File

@@ -1,5 +0,0 @@
CMakeLists.txt
.idea
cmake-build-debug
node_modules
build

View File

@@ -1,42 +0,0 @@
{
"targets": [
{
"target_name": "native",
"sources": [
"src/main.cc",
"src/utils.h",
"src/utils.cc",
"src/define.h",
"src/wmi/wmi.cpp",
"src/wmi/wmi.hpp",
"src/wmi/unistd.h",
"src/wmi/wmiresult.cpp",
"src/wmi/wmiresult.hpp",
"src/wmi/wmiclasses.hpp",
"src/wmi/wmiexception.hpp",
"src/registry/registry.hpp"
],
"cflags!": [
"-fno-exceptions"
],
"cflags_cc!": [
"-fno-exceptions"
],
"defines": [
"NAPI_DISABLE_CPP_EXCEPTIONS"
],
"include_dirs": [
"<!(node -p \"require('node-addon-api').include_dir\")"
],
"msvs_settings": {
"VCCLCompilerTool": {
"AdditionalOptions": [
"-std:c++latest",
"-DUNICODE",
"-sdl"
]
}
}
}
]
}

View File

@@ -1,15 +0,0 @@
{
"name": "genshin-export-native",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "node-gyp rebuild && copy .\\build\\Release\\native.node ..\\genshin-export\\"
},
"gypfile": true,
"devDependencies": {
"node-gyp": "^9.0.0"
},
"dependencies": {
"node-addon-api": "^4.3.0"
}
}

View File

@@ -1,14 +0,0 @@
#pragma once
#include <string>
#include <iostream>
#include <napi.h>
#include <Windows.h>
#include <TlHelp32.h>
using std::string, std::wstring, std::cout, std::to_string;
using Napi::Object, Napi::Env, Napi::Function, Napi::Value, Napi::CallbackInfo, Napi::TypeError, Napi::Error;
typedef unsigned char byte;
typedef unsigned long ul;
typedef unsigned long long ull;

View File

@@ -1,177 +0,0 @@
#include "utils.h"
#include "define.h"
#include "wmi/wmi.hpp"
#include "wmi/wmiclasses.hpp"
#include "registry/registry.hpp"
#include <iphlpapi.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"iphlpapi.lib")
using Wmi::Win32_ComputerSystem, Wmi::Win32_OperatingSystem, Wmi::retrieveWmi;
namespace native {
Value checkGameIsRunning(const CallbackInfo &info) {
Env env = info.Env();
if (info.Length() != 1 || !info[0].IsString()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Undefined();
}
wstring name = StringToWString(info[0].As<Napi::String>().Utf8Value());
bool isRunning = false;
PROCESSENTRY32 entry;
entry.dwSize = sizeof(entry);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (Process32First(snapshot, &entry) == TRUE) {
while (Process32Next(snapshot, &entry) == TRUE) {
if (wstring(entry.szExeFile) == name) {
isRunning = true;
}
}
}
CloseHandle(snapshot);
return Napi::Boolean::New(env, isRunning);
}
Value selectGameExecutable(const CallbackInfo &info) {
Env env = info.Env();
Napi::String path;
if (OpenFile(env, path) != ERROR_SUCCESS) {
Error::New(env, "Failed to open file: " + to_string(CommDlgExtendedError())).ThrowAsJavaScriptException();
return env.Undefined();
}
return path;
}
Value whoUseThePort(const CallbackInfo &info) {
Env env = info.Env();
if (info.Length() != 1 || !info[0].IsNumber()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Undefined();
}
DWORD dwSize = 0;
PMIB_TCPTABLE_OWNER_PID pTcpTable = nullptr;
GetExtendedTcpTable(pTcpTable, &dwSize, TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0);
pTcpTable = (PMIB_TCPTABLE_OWNER_PID)new byte[dwSize];
if(GetExtendedTcpTable(pTcpTable,&dwSize,TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0) != NO_ERROR) {
Error::New(env, "GetExtendedTcpTable failed").ThrowAsJavaScriptException();
return env.Undefined();
}
int port = info[0].As<Napi::Number>().Int32Value();
auto nNum = (int)pTcpTable->dwNumEntries;
DWORD pid = 0;
for(int i = 0; i < nNum; i++) {
if (htons(pTcpTable->table[i].dwLocalPort) == port) {
pid = pTcpTable->table[i].dwOwningPid;
break;
}
}
delete pTcpTable;
Value ret = env.Undefined();
if (pid != 0) {
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (hProcess == nullptr) {
Error::New(env, "OpenProcess error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return env.Undefined();
}
TCHAR fnBuf[MAX_PATH];
DWORD length = MAX_PATH;
if (QueryFullProcessImageName(hProcess, 0, fnBuf, &length) == 0) {
Error::New(env, "QueryFullProcessImageName error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return env.Undefined();
}
Object obj = Object::New(env);
obj.Set("pid", Napi::Number::New(env, pid));
obj.Set("path", Napi::String::New(env, WStringToString(fnBuf)));
ret = obj;
}
return ret;
}
Value getDeviceID(const CallbackInfo &info) {
Env env = info.Env();
wstring wd;
if (RegUtils::GetString(HKEY_CURRENT_USER, L"SOFTWARE\\miHoYoSDK", L"MIHOYOSDK_DEVICE_ID", wd) != ERROR_SUCCESS) {
return env.Undefined();
}
string id = WStringToString(wd);
return Napi::String::New(env, id.substr(0, 8) + "-" + id.substr(8, 4) + "-" + id.substr(12, 4) + "-" + id.substr(16, 4) + "-" + id.substr(20, 12));
}
Value getDeviceInfo(const CallbackInfo &info) {
Env env = info.Env();
HDC desktop = GetDC(nullptr);
int sw = GetDeviceCaps(desktop, DESKTOPHORZRES);
int sh = GetDeviceCaps(desktop, DESKTOPVERTRES);
ReleaseDC(nullptr, desktop);
DWORD buildNum = RegUtils::GetInt(env, HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR");
wstring locale;
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International", L"LocaleName", locale) != ERROR_SUCCESS) {
locale = L"zh-CN";
}
wstring country;
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International\\Geo", L"Name", country) != ERROR_SUCCESS) {
country = L"CN";
}
Object obj = Object::New(env);
obj.Set("locale", Napi::String::New(env, WStringToString(locale)));
obj.Set("screenSize", Napi::String::New(env, to_string(sw) + "x" + to_string(sh)));
obj.Set("carrierCountry", Napi::String::New(env, WStringToString(country)));
try {
auto computer = retrieveWmi<Win32_ComputerSystem>();
obj.Set("model", Napi::String::New(env, computer.Model));
obj.Set("oemName", Napi::String::New(env, computer.Manufacturer));
} catch (Wmi::WmiException &e) {
obj.Set("model", Napi::String::New(env, "Unknown"));
obj.Set("oemName", Napi::String::New(env, "Unknown"));
}
try {
auto os = retrieveWmi<Win32_OperatingSystem>();
string osv = os.Version;
obj.Set("osBuild", Napi::String::New(env, osv + "." + to_string(buildNum)));
obj.Set("osVersion", Napi::String::New(env, osv));
obj.Set("timeZoneOffset", Napi::Number::New(env, os.CurrentTimeZone));
} catch (Wmi::WmiException &e) {
obj.Set("osBuild", Napi::String::New(env, "Unknown"));
obj.Set("osVersion", Napi::String::New(env, "Unknown"));
obj.Set("timeZoneOffset", Napi::Number::New(env, 480));
}
return obj;
}
Value enablePrivilege(const CallbackInfo &info) {
Env env = info.Env();
EnablePrivilege(env, L"SeDebugPrivilege");
return env.Undefined();
}
Value copyToClipboard(const CallbackInfo &info) {
Env env = info.Env();
string text = info[0].As<Napi::String>().Utf8Value();
if (OpenClipboard(nullptr)) {
EmptyClipboard();
HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, text.length() + 1);
if (hg != nullptr) {
memcpy(GlobalLock(hg), text.c_str(), text.length() + 1);
GlobalUnlock(hg);
SetClipboardData(CF_TEXT, hg);
}
CloseClipboard();
}
return env.Undefined();
}
Object init(Env env, Object exports) {
exports.Set("getDeviceID", Function::New(env, getDeviceID));
exports.Set("getDeviceInfo", Function::New(env, getDeviceInfo));
exports.Set("whoUseThePort", Function::New(env, whoUseThePort));
exports.Set("copyToClipboard", Function::New(env, copyToClipboard));
exports.Set("enablePrivilege", Function::New(env, enablePrivilege));
exports.Set("checkGameIsRunning", Function::New(env, checkGameIsRunning));
exports.Set("selectGameExecutable", Function::New(env, selectGameExecutable));
return exports;
}
NODE_API_MODULE(addon, init)
}

View File

@@ -1,40 +0,0 @@
#pragma once
#ifndef REGISTRY_HPP
#define REGISTRY_HPP
#include "../define.h"
namespace RegUtils {
DWORD GetInt(Env env, HKEY hKey, const wstring &path, const wstring &value) {
DWORD data = 0;
DWORD size = sizeof(DWORD);
LSTATUS retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_DWORD, nullptr, &data, &size);
if (retcode != ERROR_SUCCESS) {
Error::New(env, "RegGetValue error: " + to_string(retcode)).ThrowAsJavaScriptException();
return 0;
}
return data;
}
LSTATUS GetString(HKEY hKey, const wstring &path, const wstring &value, wstring &result) {
DWORD size = 0;
LSTATUS retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_SZ, nullptr, nullptr, &size);
if (retcode != ERROR_SUCCESS) {
return retcode;
}
wstring data;
DWORD len = size / sizeof(WCHAR);
data.resize(len);
retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_SZ, nullptr, &data[0], &size);
if (retcode != ERROR_SUCCESS) {
return retcode;
}
data.resize((len-1));
result = data;
return ERROR_SUCCESS;
}
}
#endif //REGISTRY_HPP

View File

@@ -1,80 +0,0 @@
#include "utils.h"
#include "define.h"
wstring StringToWString(const string &src) {
wstring result;
int len = MultiByteToWideChar(CP_ACP, 0, src.c_str(), src.size(), nullptr, 0); // NOLINT(cppcoreguidelines-narrowing-conversions)
auto *buffer = new WCHAR[len + 1];
MultiByteToWideChar(CP_ACP, 0, src.c_str(), src.size(), buffer, len); // NOLINT(cppcoreguidelines-narrowing-conversions)
buffer[len] = '\0';
result.append(buffer);
delete[] buffer;
return result;
}
string WStringToString(const wstring &src) {
string result;
int len = WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), nullptr, 0, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
char *buffer = new char[len + 1];
WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), buffer, len, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
buffer[len] = '\0';
result.append(buffer);
delete[] buffer;
return result;
}
void Log(Env env, const string &msg) {
auto logFunc = env.Global().Get("console").As<Object>().Get("log").As<Function>();
logFunc.Call({ Napi::String::New(env, msg) });
}
void Log(Env env, const wstring &msg) {
auto logFunc = env.Global().Get("console").As<Object>().Get("log").As<Function>();
logFunc.Call({ Napi::String::New(env, WStringToString(msg)) });
}
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent) {
OPENFILENAME open;
ZeroMemory(&open, sizeof(open));
WCHAR file[32768];
file[0]=L'\0';
open.Flags = OFN_FILEMUSTEXIST | OFN_NONETWORKBUTTON | OFN_PATHMUSTEXIST | OFN_EXPLORER;
open.hwndOwner = parent;
open.nMaxFile = 32768;
open.lpstrFile = file;
open.lpstrTitle = L"选择原神主程序";
open.lpstrFilter = L"国服/国际服主程序 (YuanShen/GenshinImpact.exe)\0YuanShen.exe;GenshinImpact.exe\0";
open.lStructSize = sizeof(open);
if(GetOpenFileName(&open)) {
result = Napi::String::New(env, WStringToString(file));
return ERROR_SUCCESS;
} else {
return ERROR_ERRORS_ENCOUNTERED;
}
}
BOOL EnablePrivilege(Env env, const wstring &name) {
HANDLE hToken;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) {
Error::New(env, "OpenProcessToken error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return FALSE;
}
TOKEN_PRIVILEGES tp;
ZeroMemory(&tp, sizeof(tp));
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValue(nullptr, name.c_str(), &tp.Privileges[0].Luid)) {
Error::New(env, "LookupPrivilegeValue error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return FALSE;
}
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr)) {
Error::New(env, "AdjustTokenPrivileges error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
Error::New(env, "The token does not have the specified privilege.").ThrowAsJavaScriptException();
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}

View File

@@ -1,15 +0,0 @@
#pragma once
#include "define.h"
string WStringToString(const wstring &src);
wstring StringToWString(const string &src);
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent = GetConsoleWindow());
BOOL EnablePrivilege(Env env, const wstring &name);
void Log(Env env, const string &msg);
void Log(Env env, const wstring &msg);
#ifndef GENSHIN_EXPORT_NATIVE_UTILS_H
#define GENSHIN_EXPORT_NATIVE_UTILS_H
#endif //GENSHIN_EXPORT_NATIVE_UTILS_H

Submodule native/src/wmi deleted from aab36ea1e1

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,102 +0,0 @@
syntax = "proto3";
message AllAchievement {
repeated Achievement list = 1;
}
message Achievement {
enum Status {
INVALID = 0;
UNFINISHED = 1;
FINISHED = 2;
REWARD_TAKEN = 3;
}
uint32 id = 1;
Status status = 2;
uint32 current = 3;
uint32 require = 4;
uint32 finish_timestamp = 5;
}
message QueryCurRegion {
int32 retcode = 1;
string message = 2;
msg0 info = 3;
oneof group {
msg3 field2 = 4;
msg4 field3 = 5;
}
bytes field4 = 11;
bytes field5 = 12;
bytes field6 = 13;
}
message QueryRegionList {
int32 retcode = 1;
bytes field0 = 5;
bytes field1 = 6;
bool field2 = 7;
repeated msg2 list = 2;
}
message msg0 {
string ip = 1;
uint32 port = 2;
string field0 = 3;
string field1 = 7;
string field2 = 8;
string field3 = 9;
string field4 = 10;
string field5 = 11;
string field6 = 12;
string field7 = 13;
uint32 field8 = 14;
string field9 = 16;
uint32 fieldA = 18;
string fieldB = 19;
string fieldC = 20;
msg1 fieldD = 22;
bytes fieldE = 23;
string fieldF = 24;
string fieldG = 26;
string fieldH = 27;
bool fieldI = 28;
string fieldJ = 29;
string fieldK = 30;
string fieldL = 31;
string fieldM = 32;
string fieldN = 33;
string fieldO = 34;
msg1 fieldP = 35;
}
message msg1 {
uint32 field0 = 1;
bool field1 = 2;
string field2 = 3;
string field3 = 4;
string field4 = 5;
string field5 = 6;
string field6 = 7;
}
message msg2 {
string field0 = 1;
string field1 = 2;
string field2 = 3;
string url = 4;
}
message msg3 {
string field0 = 1;
}
message msg4 {
uint32 field0 = 1;
uint32 field1 = 2;
string field2 = 3;
string field3 = 4;
}

View File

@@ -1,111 +0,0 @@
const fs = require("fs")
const https = require("https")
const axios = require("axios")
const { decodeProto, encodeProto, debug } = require("./utils")
const path = require("path")
const cert = path.join(__dirname, "./cert/root.p12")
const preparedRegions = {}
let currentProxy = undefined
const getModifiedRegionList = async (conf) => {
const d = await axios.get(`https://${conf.dispatchUrl}/query_region_list`, {
responseType: "text",
params: {
version: conf.version,
channel_id: conf.channel,
sub_channel_id: conf.subChannel
}
}).catch(_ => {
console.log("网络错误,请检查网络后重试 (22-1)")
process.exit(221)
})
const regions = await decodeProto(Buffer.from(d.data,"base64"),"QueryRegionList")
if (regions["retcode"] !== 0) {
console.log(`系统错误,请稍后重试 (${regions["retcode"]}-23)`)
process.exit(23)
}
regions.list = regions.list.map(item => {
const host = new URL(item.url).host
if (regions.list.length === 1) {
preparedRegions[host] = true
}
item.url = `https://localdispatch.yuanshen.com/query_cur_region/${host}`
return item
})
return (await encodeProto(regions,"QueryRegionList")).toString("base64")
}
const getModifiedRegionInfo = async (url, uc, hs) => {
const splitUrl = url.split("?")
const host = splitUrl[0].split("/")[2]
const noQueryRequest = splitUrl[1] === undefined
const query = noQueryRequest ? "" : `?${splitUrl[1]}`
const d = await axios.get(`https://${host}/query_cur_region${query}`, {
responseType: "text"
}).catch(_ => {
console.log("网络错误,请检查网络后重试 (22-2)")
process.exit(222)
})
if (noQueryRequest) {
preparedRegions[host] = true
return d.data
} else {
const region = await decodeProto(Buffer.from(d.data,"base64"),"QueryCurRegion")
if (region["retcode"] !== 0) {
console.log(`${region["message"]} (${region["retcode"]}-24)`)
process.exit(24)
}
const info = region.info
if (preparedRegions[host]) {
if (currentProxy !== undefined) {
currentProxy.close()
}
debug("Create udp proxy: %s:%d", info.ip, info.port)
currentProxy = uc(info.ip, info.port, hs)
} else {
preparedRegions[host] = true
}
info.ip = "127.0.0.1"
info.port = 45678
return (await encodeProto(region,"QueryCurRegion")).toString("base64")
}
}
const agent = new https.Agent({
rejectUnauthorized: false
})
const create = async (conf, regionListLoadedCallback, regionSelectCallback) => {
const regions = await getModifiedRegionList(conf)
const hServer = https.createServer({
pfx: fs.readFileSync(cert),
passphrase: ""
}, async (request, response) => {
const url = request.url
debug("HTTP: %s", url)
response.writeHead(200, { "Content-Type": "text/html" })
if (url.startsWith("/query_region_list")) {
response.end(regions)
} else if (url.startsWith("/query_cur_region")) {
const regionInfo = await getModifiedRegionInfo(url, regionSelectCallback, hServer)
response.end(regionInfo)
} else {
const frontResponse = await axios.get(`https://${conf.dispatchIP}${url}`, {
responseType: "arraybuffer",
httpsAgent: agent
}).catch(err => {
console.log("网络错误,请检查网络后重试 (22-3)")
console.log(err.message)
process.exit(223)
})
response.end(frontResponse.data)
}
}).listen(443, "127.0.0.1", () => {
regionListLoadedCallback()
})
}
module.exports = {
create
}

310
utils.js
View File

@@ -1,310 +0,0 @@
const fs = require("fs")
const dns = require("dns")
const ini = require("ini")
const zlib = require("zlib")
const cloud = require("./secret")
const native = require("./native")
const readline = require("readline")
const protobuf = require("protobufjs")
const { version } = require("./version")
const { promisify } = require("util")
const { createHash } = require("crypto")
const path = require("path")
const messages = path.join(__dirname, "./proto/Messages.proto")
let axios = require("axios")
const encodeProto = (object, name) => protobuf.load(messages).then(r => {
const msgType = r.lookupType(name)
const msgInst = msgType.create(object)
return msgType.encode(msgInst).finish()
})
const decodeProto = (buf, name) => protobuf.load(messages).then(r => {
return r.lookupType(name).decode(buf)
})
const checkPath = path => new Promise((resolve, reject) => {
if (!fs.existsSync(`${path}/UnityPlayer.dll`) || !fs.existsSync(`${path}/pkg_version`)) {
reject(`路径有误 ${path}`)
} else {
resolve(path)
}
})
let conf
const initConfig = async () => {
const configFileName = "./config.json"
if (fs.existsSync(configFileName)) {
conf = JSON.parse(fs.readFileSync(configFileName, "utf-8"))
} else {
conf = {
path: [],
offlineResource: false,
customCDN: ""
}
const p = path.dirname(native.selectGameExecutable())
await checkPath(p).catch(reason => {
console.log(reason)
process.exit(1)
})
conf.path.push(p)
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
}
if (conf.proxy !== undefined) {
axios = axios.create({
proxy: conf.proxy
})
}
if (conf.path.length === 1) {
await checkPath(conf.path[0]).catch(reason => {
console.log(reason)
process.exit(1)
}).then(p => {
conf.path = p
})
} else {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const question = (query) => new Promise(resolve => {
rl.question(query, resolve)
})
const idx = await question(`选择客户端: \n${conf.path.map((s, i) => {
const fp = fs.existsSync(`${s}/GenshinImpact.exe`) ? `${s}\\GenshinImpact.exe` : `${s}\\YuanShen.exe`
return `[${i}] ${fp}`
}).join("\n")}\n> `)
await checkPath(conf.path[parseInt(idx)]).catch(reason => {
console.log(reason)
process.exit(1)
}).then(p => {
conf.path = p
})
rl.close()
}
conf.isOversea = fs.existsSync(conf.path + "/GenshinImpact.exe")
conf.dataDir = conf.isOversea ? conf.path + "/GenshinImpact_Data" : conf.path + "/YuanShen_Data"
const readGameRes = (path) => fs.readFileSync(conf.dataDir + path, "utf-8")
const genshinConf = ini.parse(fs.readFileSync(`${conf.path}/config.ini`, "utf-8"))["General"]
conf.channel = genshinConf["channel"]
conf.subChannel = genshinConf["sub_channel"]
conf.version = readGameRes("/Persistent/ChannelName") + readGameRes("/Persistent/ScriptVersion")
conf.executable = conf.isOversea ? `${conf.path}/GenshinImpact.exe` : `${conf.path}/YuanShen.exe`
conf.dispatchUrl = `dispatch${conf.isOversea ? "os" : "cn"}global.yuanshen.com`
conf.dispatchIP = (await promisify(dns.lookup).bind(dns)(conf.dispatchUrl, 4)).address
return conf
}
const checkGameIsRunning = () => {
const name = path.basename(conf.executable)
if (native.checkGameIsRunning(name)) {
console.log("原神正在运行,请关闭后重试")
process.exit(301)
}
}
const checkPortIsUsing = () => {
const portUsage = native.whoUseThePort(443)
if (portUsage !== undefined) {
console.log(`443端口被 ${path.basename(portUsage["path"])}(${portUsage["pid"]}) 占用,结束该进程后重试`)
process.exit(14)
}
}
const splitPacket = buf => {
let offset = 0
let arr = []
while (offset < buf.length) {
let dataLength = buf.readUInt32LE(offset + 24)
arr.push(buf.subarray(offset, offset + 28 + dataLength))
offset += dataLength + 28
}
return arr
}
const md5 = str => {
const h = createHash("md5")
h.update(str)
return h.digest("hex")
}
let cdnUrlFormat = null
String.prototype.format = function() {
const args = arguments;
return this.replace(/{(\d+)}/g, (match, number) => typeof args[number] != "undefined" ? args[number] : match)
}
const checkCDN = async () => {
try {
cdnUrlFormat = "https://cdn.jsdelivr.net/gh/{0}@master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
try {
cdnUrlFormat = "https://raw.githubusercontent.com/{0}/master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
try {
const s = conf === undefined ? "" : conf.customCDN.trim()
if (s.length > 0) {
cdnUrlFormat = s
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
}
} catch (e) {}
try {
cdnUrlFormat = "https://raw.fastgit.org/{0}/master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
try {
cdnUrlFormat = "https://ghproxy.net/https://raw.githubusercontent.com/{0}/master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
throw "网络错误,请检查配置文件/网络后重试 (11-3)"
}
const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
log(`预检资源: ${cdnUrlFormat.format(repo, fp)}`)
fs.mkdirSync("./cache", { recursive: true })
const localPath = `./cache/${md5(fp)}`
if (conf.offlineResource) {
const fd = brotliDecompressSync(fs.readFileSync(localPath))
return JSON.parse(fd.subarray(1 + fd.readUInt8()).toString())
}
const header = {}
let fd = Buffer.alloc(0)
if (fs.existsSync(localPath)) {
fd = brotliDecompressSync(fs.readFileSync(localPath))
const etagLength = fd.readUInt8()
header["If-None-Match"] = fd.subarray(1, 1 + etagLength).toString()
}
const headResponse = await axios.head(cdnUrlFormat.format(repo, fp), {
headers: header,
validateStatus: _ => true
})
if (headResponse.status === 304) {
log("%s 命中缓存", fp)
const etagLength = fd.readUInt8()
return JSON.parse(fd.subarray(1 + etagLength).toString())
} else {
log("下载所需资源, 请稍后...")
const response = await axios.get(cdnUrlFormat.format(repo, fp))
const etag = response.headers.etag
const str = JSON.stringify(response.data)
const comp = brotliCompressSync(Buffer.concat([Buffer.of(etag.length), Buffer.from(etag), Buffer.from(str)]))
fs.writeFileSync(localPath, comp)
log("下载完成")
return response.data
}
}
const isDebug = false
const debug = (msg, ...params) => {
if (isDebug) log(msg, ...params)
}
const log = (msg, ...params) => {
const time = new Date()
const timeStr = time.getHours().toString().padStart(2, "0") + ":" + time.getMinutes().toString().padStart(2, "0") + ":" + time.getSeconds().toString().padStart(2, "0")
console.log(`${timeStr} ${msg}`, ...params)
}
const upload = async data => {
return await cloud.post("/achievement-export", data)
}
const checkUpdate = async () => {
const data = (await cloud.get("/latest-version")).data
if (data["vc"] !== version.code) {
log(`有可用更新: ${version.name} => ${data["vn"]}`)
log(`更新内容: \n${data["ds"]}`)
log("下载地址: https://github.com/HolographicHat/genshin-achievement-export/releases\n")
}
}
const brotliCompressSync = data => zlib.brotliCompressSync(data,{
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: data.length
}
})
const brotliDecompressSync = data => zlib.brotliDecompressSync(data)
let hostsContent = undefined
const setupHost = (restore = false) => {
if (restore && hostsContent === undefined) return
const path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
if (!fs.existsSync(path)) {
fs.writeFileSync(path, "")
}
fs.chmodSync(path, 0o777)
if (restore) {
fs.writeFileSync(path, hostsContent)
} else {
hostsContent = fs.readFileSync(path, "utf-8")
const requireHosts = new Map()
requireHosts.set(conf.dispatchUrl, "127.0.0.1")
requireHosts.set("localdispatch.yuanshen.com", "127.0.0.1")
const currentHosts = new Map()
hostsContent.split("\n").map(l => l.trim()).filter(l => !l.startsWith("#") && l.length > 0).forEach(value => {
const pair = value.trim().split(" ").filter(v => v.trim().length !== 0)
currentHosts.set(pair[1], pair[0])
})
requireHosts.forEach((value, key) => {
if (currentHosts.has(key)) {
if (currentHosts.get(key) === value) {
requireHosts.delete(key)
} else {
currentHosts.delete(key)
}
}
})
requireHosts.forEach((ip, host) => {
currentHosts.set(host, ip)
})
const newContent = Array.from(currentHosts.entries()).map(pair => {
return `${pair[1]} ${pair[0]}`
}).join("\n")
fs.writeFileSync(path, newContent)
}
debug("修改SystemHosts")
process.on("exit", () => {
fs.writeFileSync(path, hostsContent)
})
}
// noinspection JSUnusedGlobalSymbols
class KPacket {
constructor(data) {
this.origin = data
this.conv = data.readUInt32BE(0)
this.token = data.readUInt32BE(4)
this.cmd = data.readUInt8(8)
this.frg = data.readUInt8(9)
this.wnd = data.readUInt16LE(10)
this.ts = data.readUInt32LE(12)
this.sn = data.readUInt32LE(16)
this.una = data.readUInt32LE(20)
this.length = data.readUInt32LE(24)
this.data = data.subarray(28)
this.hash = (() => {
const h = createHash("sha256")
h.update(Buffer.concat([Buffer.of(this.sn, this.frg), this.data]))
return h.digest("hex")
})()
}
}
module.exports = {
log, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat, checkGameIsRunning, checkPortIsUsing
}

View File

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