mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-16 12:57:44 +01:00
Compare commits
641 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2deeacd443 | ||
|
|
9c73cbe596 | ||
|
|
dbe61a426e | ||
|
|
eb8e431267 | ||
|
|
87a0c69020 | ||
|
|
7752b0f918 | ||
|
|
fd85a8d3bc | ||
|
|
f01971a7d7 | ||
|
|
1779d2681a | ||
|
|
4651397808 | ||
|
|
1ba18e54bf | ||
|
|
907b585b75 | ||
|
|
b963e83cba | ||
|
|
ec450da054 | ||
|
|
990c4fd7e8 | ||
|
|
b1b99bae13 | ||
|
|
c4faf84a2d | ||
|
|
abe27891c3 | ||
|
|
1286dbd279 | ||
|
|
0f14f5dab7 | ||
|
|
49e281e573 | ||
|
|
0a070970a0 | ||
|
|
3de8c511bf | ||
|
|
e2297661f3 | ||
|
|
8285aa1014 | ||
|
|
256ab9dc9c | ||
|
|
332d0d0cf5 | ||
|
|
78912c1552 | ||
|
|
28e39ab9e2 | ||
|
|
526e651750 | ||
|
|
4a33d34a3f | ||
|
|
0aa746e3bf | ||
|
|
5044aeda2b | ||
|
|
34f13b3823 | ||
|
|
73447f205d | ||
|
|
0490a71990 | ||
|
|
2b347eaff9 | ||
|
|
f4defb735b | ||
|
|
4a75fe73df | ||
|
|
b30a93816b | ||
|
|
7d2f12c6e2 | ||
|
|
fc2220c4d9 | ||
|
|
d3b9c75e50 | ||
|
|
bcf4f396d6 | ||
|
|
dc77235c96 | ||
|
|
d8a13a72aa | ||
|
|
9e18b843db | ||
|
|
dc783e0c2b | ||
|
|
33a7cdefa8 | ||
|
|
aa4ace976e | ||
|
|
252b7eeb9b | ||
|
|
73edaadbca | ||
|
|
934df7da8a | ||
|
|
2b51a2a54e | ||
|
|
5c250c1725 | ||
|
|
470267a185 | ||
|
|
5c7a5295d1 | ||
|
|
e598013e30 | ||
|
|
c0077b1e26 | ||
|
|
10ef40ddf5 | ||
|
|
5da79a7dba | ||
|
|
3abf7bb00b | ||
|
|
afa7b0c1f3 | ||
|
|
8f8f4faa12 | ||
|
|
672636c3bf | ||
|
|
b9c4c97eba | ||
|
|
ac7c4e889a | ||
|
|
951290cac7 | ||
|
|
61423f1791 | ||
|
|
b601bfdbfb | ||
|
|
3b8f0bc92f | ||
|
|
c1df0da9be | ||
|
|
214d9027b5 | ||
|
|
745b3a4939 | ||
|
|
f3694a41ff | ||
|
|
39e60f27f2 | ||
|
|
a03e37f700 | ||
|
|
c545205e66 | ||
|
|
0c2ce097ed | ||
|
|
d689c4763a | ||
|
|
dd94d10722 | ||
|
|
8bb6cdd8d6 | ||
|
|
fab7eef244 | ||
|
|
5bfbcbb8f5 | ||
|
|
55eb7e41d8 | ||
|
|
b5028add57 | ||
|
|
5ee339b5a8 | ||
|
|
7fb43f8707 | ||
|
|
b2fb6949d2 | ||
|
|
6c8b2b4a6d | ||
|
|
f635673ce9 | ||
|
|
156abbdcbe | ||
|
|
47f60eb391 | ||
|
|
ef0d680f06 | ||
|
|
8afc02b364 | ||
|
|
035be9d67d | ||
|
|
86396946e9 | ||
|
|
b29b7851d9 | ||
|
|
90c29e5646 | ||
|
|
290ad9fc41 | ||
|
|
0cc5d301e5 | ||
|
|
c93f04f0e4 | ||
|
|
9b9a66bdd2 | ||
|
|
e94ded628a | ||
|
|
d0caf98eb3 | ||
|
|
27414d33dd | ||
|
|
bd05f4c1a5 | ||
|
|
bcc16c9b0e | ||
|
|
36c3429566 | ||
|
|
1398054216 | ||
|
|
6e19aca481 | ||
|
|
790669e60a | ||
|
|
9b55b020ca | ||
|
|
8b0f0fb44e | ||
|
|
09a1fd1925 | ||
|
|
5fe6df3887 | ||
|
|
8c26d67739 | ||
|
|
e4ef56b878 | ||
|
|
e44fda1911 | ||
|
|
a1d2e275a7 | ||
|
|
9538af0554 | ||
|
|
5a0257e40e | ||
|
|
bbb6e438b1 | ||
|
|
e32e4a0c8e | ||
|
|
49abb19673 | ||
|
|
79ce2fff0a | ||
|
|
fc130e325c | ||
|
|
a659cd8a49 | ||
|
|
62b8b0834c | ||
|
|
61ba319e98 | ||
|
|
392e027ae3 | ||
|
|
689d2f01d9 | ||
|
|
558a011e00 | ||
|
|
c00363badf | ||
|
|
c559426d8b | ||
|
|
31cbf4d8eb | ||
|
|
65c604f827 | ||
|
|
bf75937cc0 | ||
|
|
186b1b8376 | ||
|
|
f76d77f79d | ||
|
|
9da178ad56 | ||
|
|
3aca09d0fb | ||
|
|
b2397efb25 | ||
|
|
282fa87571 | ||
|
|
3be14d4135 | ||
|
|
8ccfac2318 | ||
|
|
e2a18dee5e | ||
|
|
f803dfbd44 | ||
|
|
4dcfa9da98 | ||
|
|
867ae80b22 | ||
|
|
1b39040072 | ||
|
|
ea1bb92d17 | ||
|
|
5513bd1633 | ||
|
|
f307aded73 | ||
|
|
a59efbd84c | ||
|
|
c3952bbf53 | ||
|
|
5a8bb73b39 | ||
|
|
96b5ad1b65 | ||
|
|
69c24fdbb9 | ||
|
|
3abddbae2c | ||
|
|
b24cadf2d8 | ||
|
|
9205529820 | ||
|
|
da2b80156a | ||
|
|
1f5f6f8914 | ||
|
|
08c4f7dbdd | ||
|
|
451db117a6 | ||
|
|
73168b0e53 | ||
|
|
3ef6135f15 | ||
|
|
6374c0d6ae | ||
|
|
e603af5acc | ||
|
|
8b0bb343f9 | ||
|
|
bc2eac6006 | ||
|
|
66fde2d458 | ||
|
|
c7dd694a53 | ||
|
|
efed9ca20b | ||
|
|
8a49a5ee48 | ||
|
|
a3d930b8e2 | ||
|
|
5e4ad4a694 | ||
|
|
c71d8889d7 | ||
|
|
4ddaaf3809 | ||
|
|
3b8917bcc8 | ||
|
|
517d5d017b | ||
|
|
8fb2c39d80 | ||
|
|
89c46944b6 | ||
|
|
cb3881f07d | ||
|
|
6ead1c8895 | ||
|
|
f3f4ced049 | ||
|
|
7af0523e88 | ||
|
|
7eea7d6182 | ||
|
|
86e12f411d | ||
|
|
db5f27518f | ||
|
|
c005bae265 | ||
|
|
19fca721e9 | ||
|
|
a56d2cf40b | ||
|
|
3eb65c85c0 | ||
|
|
1b76aec89f | ||
|
|
b52024d927 | ||
|
|
56b0ae80b6 | ||
|
|
0b1a697d4d | ||
|
|
05beea003c | ||
|
|
3c8cef06dd | ||
|
|
0d533c18f8 | ||
|
|
52166e4b9e | ||
|
|
ea756250a8 | ||
|
|
984bdbcf0e | ||
|
|
bd87a3d251 | ||
|
|
77b6d71855 | ||
|
|
6550e71092 | ||
|
|
7ac12627ec | ||
|
|
17c0527f2d | ||
|
|
3a1e1e6425 | ||
|
|
37fa40ab58 | ||
|
|
fb7cd452f6 | ||
|
|
7f4352dc43 | ||
|
|
707369bfad | ||
|
|
25dba5e23b | ||
|
|
05037dccc7 | ||
|
|
574e0d4582 | ||
|
|
02d4081f2f | ||
|
|
355ad64eb9 | ||
|
|
f1f95eda09 | ||
|
|
fc804ba0d0 | ||
|
|
108a7a2c2d | ||
|
|
c5d90aef64 | ||
|
|
92d6c70358 | ||
|
|
2fc9884aad | ||
|
|
b3c4363e0f | ||
|
|
841cdf52bd | ||
|
|
19660a20d9 | ||
|
|
f142fb1058 | ||
|
|
46954e6add | ||
|
|
01901c237a | ||
|
|
cdf4e27355 | ||
|
|
a843079a6b | ||
|
|
ddd85513ba | ||
|
|
89fbe6c8b0 | ||
|
|
1c1b60efee | ||
|
|
2e7c48316f | ||
|
|
b0a0fafb53 | ||
|
|
8334836b6a | ||
|
|
8a742e7e59 | ||
|
|
56325afa7f | ||
|
|
1bff6abae9 | ||
|
|
d7935d6dd4 | ||
|
|
a715725a9d | ||
|
|
bc8e986c11 | ||
|
|
ffd99d5791 | ||
|
|
20af5b40c7 | ||
|
|
a1409096fd | ||
|
|
fecba89710 | ||
|
|
b57b96b9a0 | ||
|
|
180676fe47 | ||
|
|
2d096d9b33 | ||
|
|
e100ec2abd | ||
|
|
71b0a757e9 | ||
|
|
0b55dc3e10 | ||
|
|
4d9751ea5f | ||
|
|
a39763f161 | ||
|
|
201c9cfcf2 | ||
|
|
e07bda7e58 | ||
|
|
b88a6bb616 | ||
|
|
e53ccdbcc0 | ||
|
|
97df73acea | ||
|
|
2806e59dba | ||
|
|
24caa1cb18 | ||
|
|
5d08170333 | ||
|
|
d0110f7251 | ||
|
|
2dbae05522 | ||
|
|
8ed1af30df | ||
|
|
e8485dee25 | ||
|
|
0072f49fe8 | ||
|
|
c45c6aafe1 | ||
|
|
2029a0f8a6 | ||
|
|
bcb8094c2d | ||
|
|
624191d1e0 | ||
|
|
c254c8600e | ||
|
|
61376fe84e | ||
|
|
2f5f52b572 | ||
|
|
7199bfb0a9 | ||
|
|
abcddde591 | ||
|
|
2a99108eb1 | ||
|
|
8a5f1fd96d | ||
|
|
652ff59672 | ||
|
|
094483e5a0 | ||
|
|
c50237cf66 | ||
|
|
d4fe523d73 | ||
|
|
9e5723359a | ||
|
|
07f9e03010 | ||
|
|
9cfa81c92d | ||
|
|
b35faf13b5 | ||
|
|
caa869d3ac | ||
|
|
9fd59f736d | ||
|
|
ab5ea34e68 | ||
|
|
501e30e31c | ||
|
|
3d29157391 | ||
|
|
b2d9480f9f | ||
|
|
1ad1343cbc | ||
|
|
61123ce573 | ||
|
|
9f565fafd8 | ||
|
|
88fc933e3f | ||
|
|
e032840ac8 | ||
|
|
1d1db04f04 | ||
|
|
446c7e3877 | ||
|
|
e09c43b8de | ||
|
|
9c2d2b7c1d | ||
|
|
2e5c560ed7 | ||
|
|
45366efd9f | ||
|
|
3c7dbf9f81 | ||
|
|
a36e11574b | ||
|
|
d94cacaac3 | ||
|
|
7cf20fe102 | ||
|
|
98a4c0d4fd | ||
|
|
f85ef995e3 | ||
|
|
e7d4786a1f | ||
|
|
4d949e4a07 | ||
|
|
68ca60fa8c | ||
|
|
411067219e | ||
|
|
fc983458fa | ||
|
|
ddc3113244 | ||
|
|
da7be64fdf | ||
|
|
0112e17fdb | ||
|
|
6f8e33a39c | ||
|
|
ddc743aae1 | ||
|
|
8dcbd52c22 | ||
|
|
1b5fbaa82e | ||
|
|
9bce0d33a6 | ||
|
|
879c210cc6 | ||
|
|
1fe2d54128 | ||
|
|
bfd592abbe | ||
|
|
df0bfc18c3 | ||
|
|
0480693f92 | ||
|
|
3fbc24904a | ||
|
|
5bb212bfaa | ||
|
|
f055af7f7b | ||
|
|
a917ebd856 | ||
|
|
0e6dae9f64 | ||
|
|
4fa4d7f338 | ||
|
|
f198ce46dc | ||
|
|
518b3a4fb3 | ||
|
|
85949072ec | ||
|
|
14e97a1a37 | ||
|
|
f3c826a54b | ||
|
|
fb229a0a12 | ||
|
|
85a7c60dae | ||
|
|
c923884626 | ||
|
|
78781c8988 | ||
|
|
2e24696731 | ||
|
|
b81cb9c74c | ||
|
|
8e8d0246bc | ||
|
|
d47a41b295 | ||
|
|
c9276b1771 | ||
|
|
386828005b | ||
|
|
08c1768286 | ||
|
|
eb9555ee22 | ||
|
|
be3f71dc73 | ||
|
|
e01acb4a80 | ||
|
|
f8725e5f37 | ||
|
|
c3e3e4aa85 | ||
|
|
b82b4f40ce | ||
|
|
4f59e09513 | ||
|
|
0533872a73 | ||
|
|
27a7adfdb9 | ||
|
|
54bac7f32a | ||
|
|
26f119096b | ||
|
|
c51e65e0bd | ||
|
|
874745651b | ||
|
|
ac2d522415 | ||
|
|
ead1c705a4 | ||
|
|
fadf941fa4 | ||
|
|
a31dda7865 | ||
|
|
d7e04ad4ff | ||
|
|
7510c032cc | ||
|
|
d12a9ec7da | ||
|
|
6367a66aad | ||
|
|
edc6962296 | ||
|
|
78ecb721cd | ||
|
|
b8724f7a59 | ||
|
|
d7915c7020 | ||
|
|
170f6e0859 | ||
|
|
325d28ee32 | ||
|
|
29c154f9b5 | ||
|
|
2a60bc61a7 | ||
|
|
166f249e13 | ||
|
|
c525655be6 | ||
|
|
02e0f1d36c | ||
|
|
c661faea6b | ||
|
|
4c3ba35f07 | ||
|
|
d4f1636dd2 | ||
|
|
196a5ef709 | ||
|
|
c136934aa8 | ||
|
|
c6b173dd63 | ||
|
|
5e192ef39b | ||
|
|
947518b3d6 | ||
|
|
9e5195492e | ||
|
|
2cef75bbbe | ||
|
|
1e7e7c732d | ||
|
|
efd66fd3f8 | ||
|
|
ab0500ca6f | ||
|
|
2c1bb76643 | ||
|
|
9a1fae8246 | ||
|
|
bd427d7b54 | ||
|
|
d56c7a1963 | ||
|
|
8ab7b59ae4 | ||
|
|
7b286c427c | ||
|
|
8773964de9 | ||
|
|
1f30ce4c39 | ||
|
|
ea07f41ab1 | ||
|
|
0656bff1f9 | ||
|
|
0d8f577576 | ||
|
|
01d8fc0c7e | ||
|
|
71927a8bf6 | ||
|
|
0daca30203 | ||
|
|
28941cb69e | ||
|
|
f831a7c010 | ||
|
|
05648f019b | ||
|
|
6a69a6e197 | ||
|
|
cc91916574 | ||
|
|
20041be27c | ||
|
|
b7dda599fb | ||
|
|
63b7ecf0d7 | ||
|
|
e65f441105 | ||
|
|
1822ef1808 | ||
|
|
cb441631e1 | ||
|
|
1bdad092ca | ||
|
|
f4c9c16c68 | ||
|
|
53a082e68d | ||
|
|
9ea417c9ef | ||
|
|
9001c96986 | ||
|
|
46dee9a483 | ||
|
|
d3c812ba6c | ||
|
|
7ec1de4c76 | ||
|
|
05f31265eb | ||
|
|
64d4f7061a | ||
|
|
e4eca842d3 | ||
|
|
a88247bdfc | ||
|
|
c79fa96505 | ||
|
|
596af24e95 | ||
|
|
ba0cf4c990 | ||
|
|
9a49a9588b | ||
|
|
750fa58147 | ||
|
|
19a3926051 | ||
|
|
4937a2f4bd | ||
|
|
78ed4a2b01 | ||
|
|
72dc094b57 | ||
|
|
62b9c1f2a1 | ||
|
|
a2e923b051 | ||
|
|
fea7b3676f | ||
|
|
de396e70f8 | ||
|
|
7a8f01f418 | ||
|
|
a134c6d064 | ||
|
|
9d0879148c | ||
|
|
778c82fad2 | ||
|
|
7f2ed9adb6 | ||
|
|
53b94caeb7 | ||
|
|
d1dc81318a | ||
|
|
a48eead85e | ||
|
|
d1bed3ebc5 | ||
|
|
23e7c164d8 | ||
|
|
8a9b47c7a4 | ||
|
|
520e3ea028 | ||
|
|
dd70c5b8ee | ||
|
|
2b2f628096 | ||
|
|
6340afb692 | ||
|
|
928fbba489 | ||
|
|
7bc921f543 | ||
|
|
a37a13e0ba | ||
|
|
e0eff2fe74 | ||
|
|
7d76d27555 | ||
|
|
4e87b4b007 | ||
|
|
b11b769292 | ||
|
|
dc9ff0a54c | ||
|
|
45bd30fcca | ||
|
|
f635c149a2 | ||
|
|
e1fde804ec | ||
|
|
bcce4f4216 | ||
|
|
9450f65159 | ||
|
|
bf0bd64faf | ||
|
|
4cfe561c1c | ||
|
|
f6cd6d31ff | ||
|
|
65237f84a2 | ||
|
|
dabe7d777b | ||
|
|
93b95fd813 | ||
|
|
fe163fbb97 | ||
|
|
963b3d9318 | ||
|
|
bfb07580fe | ||
|
|
af03e292ba | ||
|
|
5dd121dfcc | ||
|
|
497e61f699 | ||
|
|
5cc327c5f9 | ||
|
|
af8b61f08a | ||
|
|
700aaa4a5d | ||
|
|
69caffeb97 | ||
|
|
a06c0e3ed2 | ||
|
|
880add5ab3 | ||
|
|
193d321103 | ||
|
|
6e8efabc3b | ||
|
|
68c02caf37 | ||
|
|
878080d660 | ||
|
|
986dfa04d0 | ||
|
|
3746c47a84 | ||
|
|
c4dd75bdda | ||
|
|
5905afdf10 | ||
|
|
62fdd2c60d | ||
|
|
ba159f8c5f | ||
|
|
6ade5b21cf | ||
|
|
3c3eb9159c | ||
|
|
494d9a04fa | ||
|
|
8fd49f261a | ||
|
|
2a65d1e045 | ||
|
|
832edaf005 | ||
|
|
8a49a11dc0 | ||
|
|
32e04458c6 | ||
|
|
165060b62b | ||
|
|
fc480d8542 | ||
|
|
7751ea7185 | ||
|
|
97600b1b2c | ||
|
|
417fe39cd9 | ||
|
|
81e5793150 | ||
|
|
ce16b59f5b | ||
|
|
606d58c77e | ||
|
|
55246ab1ec | ||
|
|
5290e191a1 | ||
|
|
08a3998854 | ||
|
|
e6df536ceb | ||
|
|
76dab05cbd | ||
|
|
22f21614ae | ||
|
|
11e85c6619 | ||
|
|
90dc48f7b2 | ||
|
|
c508632150 | ||
|
|
de07a5f1b7 | ||
|
|
31340dda84 | ||
|
|
4fff7dee5a | ||
|
|
39e6186ba3 | ||
|
|
7a45c0d661 | ||
|
|
87e391958e | ||
|
|
21d4dbec66 | ||
|
|
f0568216cb | ||
|
|
8ed21b4645 | ||
|
|
0b6f3b8bcf | ||
|
|
116e8aadbc | ||
|
|
2be6566e81 | ||
|
|
49eac894a8 | ||
|
|
16fdf4e32b | ||
|
|
76afcaf426 | ||
|
|
7b723687a4 | ||
|
|
168a334756 | ||
|
|
5fd24f4bed | ||
|
|
9fb0a79885 | ||
|
|
db22c5e111 | ||
|
|
2207ad0886 | ||
|
|
ae777000e2 | ||
|
|
4a869bad3f | ||
|
|
9852feaf08 | ||
|
|
26d2f764c6 | ||
|
|
4ac4505d72 | ||
|
|
1ae7de26bf | ||
|
|
1073227b83 | ||
|
|
11adffd700 | ||
|
|
2cf869872d | ||
|
|
bcf651b5c1 | ||
|
|
a55c8ca773 | ||
|
|
153870a053 | ||
|
|
c2fc04c3a8 | ||
|
|
8cac486249 | ||
|
|
4422622e1e | ||
|
|
7bf79bdea6 | ||
|
|
7813f5d201 | ||
|
|
aa3d6c3efe | ||
|
|
d61a35b81f | ||
|
|
efaff769b5 | ||
|
|
9091216e1c | ||
|
|
9c5e4f5a32 | ||
|
|
87adb2dfb7 | ||
|
|
8edbc0ee78 | ||
|
|
d1fbee2829 | ||
|
|
191aa8d696 | ||
|
|
0bc44154aa | ||
|
|
1633e68b76 | ||
|
|
dceeccb242 | ||
|
|
2625f51021 | ||
|
|
69a8bdd638 | ||
|
|
9447708058 | ||
|
|
e2e3a01cc3 | ||
|
|
9b0c275b8b | ||
|
|
0b3a5a713e | ||
|
|
c38365bc99 | ||
|
|
3d0790e650 | ||
|
|
1cc9071ce4 | ||
|
|
ce1faa50cf | ||
|
|
374f9fcbd0 | ||
|
|
9b224857f1 | ||
|
|
0bb87d87b7 | ||
|
|
c03e7ecfe6 | ||
|
|
c264fb134e | ||
|
|
d8555f207e | ||
|
|
3f037e5d20 | ||
|
|
93a44842ed | ||
|
|
327ebf3bb3 | ||
|
|
bf4fc7864f | ||
|
|
fa58d7b3cc | ||
|
|
a2b3fb901e | ||
|
|
5b5fdc0c10 | ||
|
|
f07b308757 | ||
|
|
0047e24031 | ||
|
|
1b08d986ca | ||
|
|
29c4d4939f | ||
|
|
97196a1e35 | ||
|
|
7aa77aa2eb | ||
|
|
7f3fc5aac1 | ||
|
|
852b1289e2 | ||
|
|
e3498f1b9c | ||
|
|
80c2985795 | ||
|
|
6890e59af5 | ||
|
|
fcf5c27186 | ||
|
|
8cced4c1d7 | ||
|
|
b18b8b40e5 | ||
|
|
005699e472 | ||
|
|
ba9720cb65 | ||
|
|
1702c3be29 | ||
|
|
4a743e9060 | ||
|
|
0c9176a8b6 | ||
|
|
9e405b26d2 | ||
|
|
32cb6e2127 | ||
|
|
2affbe3683 | ||
|
|
544f8b28bf | ||
|
|
e684e7e208 | ||
|
|
f687852879 | ||
|
|
e2f3fdd0ff | ||
|
|
ef688c09e2 | ||
|
|
9092e36b33 | ||
|
|
e5451c37af | ||
|
|
40e63f2d9a | ||
|
|
f613b177a2 | ||
|
|
6337e165aa | ||
|
|
86ad1de181 | ||
|
|
98d9bf3a93 | ||
|
|
6341640243 | ||
|
|
ae58aaff30 | ||
|
|
4a62138b30 | ||
|
|
8134cde2b7 | ||
|
|
c19ea6ace3 |
608 changed files with 16908 additions and 9894 deletions
308
.github/generate_changelog.py
vendored
Normal file
308
.github/generate_changelog.py
vendored
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a changelog from git commits between the last two tags and post to Discord webhook.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import os
|
||||
from typing import List, Tuple, Optional, Dict, Any
|
||||
|
||||
|
||||
def run_git_command(args: List[str]) -> str:
|
||||
"""Run a git command and return its output."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git"] + args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Git command failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_last_two_tags() -> Tuple[str, str]:
|
||||
"""Get the latest two git tags."""
|
||||
tags = run_git_command(["tag", "--sort=-version:refname"])
|
||||
tag_list = [t for t in tags.split("\n") if t]
|
||||
|
||||
# Filter out old tags that start with 'v' (old versioning scheme)
|
||||
tag_list = [t for t in tag_list if not t.startswith('v')]
|
||||
|
||||
if len(tag_list) < 2:
|
||||
print("Error: Need at least 2 tags in the repository", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return tag_list[0], tag_list[1]
|
||||
|
||||
|
||||
def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
|
||||
"""Get the commit hash of a submodule at a specific tag."""
|
||||
try:
|
||||
# Get the submodule commit at the specified tag
|
||||
result = run_git_command(["ls-tree", tag, submodule_path])
|
||||
# Format is: "<mode> commit <hash>\t<path>"
|
||||
parts = result.split()
|
||||
if len(parts) >= 3 and parts[1] == "commit":
|
||||
return parts[2]
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_repo_info() -> Tuple[str, str]:
|
||||
"""Get repository owner and name from git remote."""
|
||||
try:
|
||||
remote_url = run_git_command(["config", "--get", "remote.origin.url"])
|
||||
|
||||
# Handle both HTTPS and SSH URLs
|
||||
# SSH: git@github.com:owner/repo.git
|
||||
# HTTPS: https://github.com/owner/repo.git
|
||||
match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url)
|
||||
if match:
|
||||
owner = match.group(1)
|
||||
repo = match.group(2)
|
||||
return owner, repo
|
||||
else:
|
||||
print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except:
|
||||
print("Error: Could not get git remote URL", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_commits_between_tags(tag1: str, tag2: str) -> List[str]:
|
||||
"""Get commit SHAs between two tags."""
|
||||
log_output = run_git_command([
|
||||
"log",
|
||||
f"{tag2}..{tag1}",
|
||||
"--format=%H"
|
||||
])
|
||||
|
||||
commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
|
||||
return commits
|
||||
|
||||
|
||||
def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get PR information for a commit using GitHub API."""
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls"
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
prs = response.json()
|
||||
|
||||
if prs and len(prs) > 0:
|
||||
# Return the first PR (most relevant one)
|
||||
pr = prs[0]
|
||||
return {
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"author": pr["user"]["login"],
|
||||
"url": pr["html_url"]
|
||||
}
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
# Commit might not be associated with a PR
|
||||
return None
|
||||
elif e.response.status_code == 403:
|
||||
print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr)
|
||||
return None
|
||||
else:
|
||||
print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]:
|
||||
"""Get PRs between two tags using GitHub API."""
|
||||
commits = get_commits_between_tags(tag1, tag2)
|
||||
print(f"Found {len(commits)} commits, fetching PR information...")
|
||||
|
||||
prs = []
|
||||
seen_pr_numbers = set()
|
||||
|
||||
for i, commit_sha in enumerate(commits, 1):
|
||||
if i % 10 == 0:
|
||||
print(f"Progress: {i}/{len(commits)} commits processed...")
|
||||
|
||||
pr_info = get_pr_for_commit(commit_sha, owner, repo, token)
|
||||
if pr_info and pr_info["number"] not in seen_pr_numbers:
|
||||
seen_pr_numbers.add(pr_info["number"])
|
||||
prs.append(pr_info)
|
||||
|
||||
return prs
|
||||
|
||||
|
||||
def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]:
|
||||
"""Filter out PRs matching any of the ignore patterns."""
|
||||
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
|
||||
|
||||
filtered = []
|
||||
for pr in prs:
|
||||
if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
|
||||
filtered.append(pr)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]],
|
||||
cs_commit_new: Optional[str], cs_commit_old: Optional[str],
|
||||
owner: str, repo: str) -> str:
|
||||
"""Generate markdown changelog."""
|
||||
# Calculate statistics
|
||||
pr_count = len(prs)
|
||||
unique_authors = len(set(pr["author"] for pr in prs))
|
||||
|
||||
changelog = f"# Dalamud Release v{version}\n\n"
|
||||
changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. "
|
||||
changelog += f"This release includes **{pr_count} PR{'s' if pr_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
|
||||
changelog += f"[Click here](<https://github.com/{owner}/{repo}/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\n"
|
||||
|
||||
if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old:
|
||||
changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
|
||||
changelog += f"[Click here](<https://github.com/aers/FFXIVClientStructs/compare/{cs_commit_old}...{cs_commit_new}>) to see all CS changes.\n"
|
||||
elif cs_commit_new:
|
||||
changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
|
||||
|
||||
changelog += "## Dalamud Changes\n\n"
|
||||
|
||||
for pr in prs:
|
||||
changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
|
||||
|
||||
return changelog
|
||||
|
||||
|
||||
def post_to_discord(webhook_url: str, content: str, version: str) -> None:
|
||||
"""Post changelog to Discord webhook as a file attachment."""
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
filename = f"changelog-v{version}.md"
|
||||
|
||||
# Prepare the payload
|
||||
data = {
|
||||
"content": f"Dalamud v{version} has been released!",
|
||||
"attachments": [
|
||||
{
|
||||
"id": "0",
|
||||
"filename": filename
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Prepare the files
|
||||
files = {
|
||||
"payload_json": (None, json.dumps(data)),
|
||||
"files[0]": (filename, content.encode('utf-8'), 'text/markdown')
|
||||
}
|
||||
|
||||
try:
|
||||
result = requests.post(webhook_url, files=files)
|
||||
result.raise_for_status()
|
||||
print(f"Successfully posted to Discord webhook, code {result.status_code}")
|
||||
except requests.exceptions.HTTPError as err:
|
||||
print(f"Failed to post to Discord: {err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Failed to post to Discord: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate changelog from git commits and post to Discord webhook"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--webhook-url",
|
||||
required=True,
|
||||
help="Discord webhook URL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--github-token",
|
||||
default=os.environ.get("GITHUB_TOKEN"),
|
||||
help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Regex patterns to ignore PRs (can be specified multiple times)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--submodule-path",
|
||||
default="lib/FFXIVClientStructs",
|
||||
help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get repository info
|
||||
owner, repo = get_repo_info()
|
||||
print(f"Repository: {owner}/{repo}")
|
||||
|
||||
# Get the last two tags
|
||||
latest_tag, previous_tag = get_last_two_tags()
|
||||
print(f"Generating changelog between {previous_tag} and {latest_tag}")
|
||||
|
||||
# Get submodule commits at both tags
|
||||
cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag)
|
||||
cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag)
|
||||
|
||||
if cs_commit_new:
|
||||
print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}")
|
||||
if cs_commit_old:
|
||||
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
|
||||
|
||||
# Get PRs between tags
|
||||
prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
|
||||
prs.reverse()
|
||||
print(f"Found {len(prs)} PRs")
|
||||
|
||||
# Filter PRs
|
||||
filtered_prs = filter_prs(prs, args.ignore)
|
||||
print(f"After filtering: {len(filtered_prs)} PRs")
|
||||
|
||||
# Generate changelog
|
||||
changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
|
||||
cs_commit_new, cs_commit_old, owner, repo)
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("Generated Changelog:")
|
||||
print("="*50)
|
||||
print(changelog)
|
||||
print("="*50 + "\n")
|
||||
|
||||
# Post to Discord
|
||||
post_to_discord(args.webhook_url, changelog, latest_tag)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
32
.github/workflows/backup.yml
vendored
Normal file
32
.github/workflows/backup.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: Back up code to other forges
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # Run every day at 2 AM
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
push-to-forges:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'goatcorp/Dalamud'
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd #v0.9.1
|
||||
with:
|
||||
ssh-private-key: |
|
||||
${{ secrets.MIRROR_GITLAB_SYNC_KEY }}
|
||||
${{ secrets.MIRROR_CODEBERG_SYNC_KEY }}
|
||||
|
||||
- name: Add remotes & push
|
||||
env:
|
||||
GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=accept-new"
|
||||
run: |
|
||||
git remote add gitlab git@gitlab.com:goatcorp/Dalamud.git
|
||||
git push gitlab --all --force
|
||||
git remote add codeberg git@codeberg.org:goatcorp/Dalamud.git
|
||||
git push codeberg --all --force
|
||||
48
.github/workflows/generate-changelog.yml
vendored
Normal file
48
.github/workflows/generate-changelog.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Generate Changelog
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
generate-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history and tags
|
||||
submodules: true # Fetch submodules
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Generate and post changelog
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
run: |
|
||||
python .github/generate_changelog.py \
|
||||
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
|
||||
--ignore "Update ClientStructs" \
|
||||
--ignore "^build:"
|
||||
|
||||
- name: Upload changelog as artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: changelog
|
||||
path: changelog-*.md
|
||||
if-no-files-found: ignore
|
||||
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
|
|
@ -3,7 +3,7 @@ on: [push, pull_request, workflow_dispatch]
|
|||
|
||||
concurrency:
|
||||
group: build_dalamud_${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
uses: microsoft/setup-msbuild@v1.0.2
|
||||
- uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '9.0.200'
|
||||
dotnet-version: '10.0.100'
|
||||
- name: Define VERSION
|
||||
run: |
|
||||
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
|
||||
|
|
|
|||
2
.github/workflows/rollup.yml
vendored
2
.github/workflows/rollup.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
branches:
|
||||
- net9
|
||||
- api14
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
|
|
|||
45
.github/workflows/update-submodules.yml
vendored
45
.github/workflows/update-submodules.yml
vendored
|
|
@ -1,16 +1,26 @@
|
|||
name: Check for FFXIVCS changes
|
||||
name: Check for Submodule Changes
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0,12,18 */1 * *"
|
||||
- cron: "0 0,6,12,18 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: FFXIVCS Check
|
||||
name: Check ${{ matrix.submodule.name }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branches: [master]
|
||||
submodule:
|
||||
- name: ClientStructs
|
||||
path: lib/FFXIVClientStructs
|
||||
branch: main
|
||||
branch-prefix: csupdate
|
||||
- name: Excel Schema
|
||||
path: lib/Lumina.Excel
|
||||
branch: master
|
||||
branch-prefix: schemaupdate
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
|
@ -24,30 +34,41 @@ jobs:
|
|||
ref: ${{ matrix.branches }}
|
||||
token: ${{ secrets.UPDATE_PAT }}
|
||||
- name: Create update branch
|
||||
run: git checkout -b csupdate/${{ matrix.branches }}
|
||||
run: git checkout -b ${{ matrix.submodule.branch-prefix }}/${{ matrix.branches }}
|
||||
- name: Initialize mandatory git config
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email noreply@github.com
|
||||
git config --global pull.rebase false
|
||||
- name: Update submodule
|
||||
id: update-submodule
|
||||
run: |
|
||||
git checkout -b csupdate-${{ matrix.branches }}
|
||||
git checkout -b ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }}
|
||||
git reset --hard origin/${{ matrix.branches }}
|
||||
cd lib/FFXIVClientStructs
|
||||
cd ${{ matrix.submodule.path }}
|
||||
git fetch
|
||||
git reset --hard origin/main
|
||||
git reset --hard origin/${{ matrix.submodule.branch }}
|
||||
cd ../..
|
||||
git add lib/FFXIVClientStructs
|
||||
git commit --message "Update ClientStructs"
|
||||
git push origin csupdate-${{ matrix.branches }} --force
|
||||
git add ${{ matrix.submodule.path }}
|
||||
|
||||
if [[ -z "$(git status --porcelain --untracked-files=no)" ]]; then
|
||||
echo "No changes detected!"
|
||||
echo "SUBMIT_PR=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit --message "Update ${{ matrix.submodule.name }}"
|
||||
git push origin ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --force
|
||||
echo "SUBMIT_PR=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create PR
|
||||
if: ${{ steps.update-submodule.outputs.SUBMIT_PR == 'true' }}
|
||||
run: |
|
||||
echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token
|
||||
prNumber=$(gh pr list --base ${{ matrix.branches }} --head csupdate-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}")
|
||||
prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}")
|
||||
if [ -z "$prNumber" ]; then
|
||||
echo "No PR found, creating one"
|
||||
gh pr create --head csupdate-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ClientStructs" --body "" --base ${{ matrix.branches }}
|
||||
gh pr create --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ${{ matrix.submodule.name }}" --body "" --base ${{ matrix.branches }}
|
||||
else
|
||||
echo "PR already exists, ignoring"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,19 +1,57 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Build Schema",
|
||||
"$ref": "#/definitions/build",
|
||||
"definitions": {
|
||||
"build": {
|
||||
"type": "object",
|
||||
"Host": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"AppVeyor",
|
||||
"AzurePipelines",
|
||||
"Bamboo",
|
||||
"Bitbucket",
|
||||
"Bitrise",
|
||||
"GitHubActions",
|
||||
"GitLab",
|
||||
"Jenkins",
|
||||
"Rider",
|
||||
"SpaceAutomation",
|
||||
"TeamCity",
|
||||
"Terminal",
|
||||
"TravisCI",
|
||||
"VisualStudio",
|
||||
"VSCode"
|
||||
]
|
||||
},
|
||||
"ExecutableTarget": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"CI",
|
||||
"Clean",
|
||||
"Compile",
|
||||
"CompileCImGui",
|
||||
"CompileCImGuizmo",
|
||||
"CompileCImPlot",
|
||||
"CompileDalamud",
|
||||
"CompileDalamudBoot",
|
||||
"CompileDalamudCrashHandler",
|
||||
"CompileImGuiNatives",
|
||||
"CompileInjector",
|
||||
"Restore",
|
||||
"SetCILogging",
|
||||
"Test"
|
||||
]
|
||||
},
|
||||
"Verbosity": {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"enum": [
|
||||
"Verbose",
|
||||
"Normal",
|
||||
"Minimal",
|
||||
"Quiet"
|
||||
]
|
||||
},
|
||||
"NukeBuild": {
|
||||
"properties": {
|
||||
"Configuration": {
|
||||
"type": "string",
|
||||
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
|
||||
"enum": [
|
||||
"Debug",
|
||||
"Release"
|
||||
]
|
||||
},
|
||||
"Continue": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates to continue a previously failed build attempt"
|
||||
|
|
@ -23,29 +61,8 @@
|
|||
"description": "Shows the help text for this build assembly"
|
||||
},
|
||||
"Host": {
|
||||
"type": "string",
|
||||
"description": "Host for execution. Default is 'automatic'",
|
||||
"enum": [
|
||||
"AppVeyor",
|
||||
"AzurePipelines",
|
||||
"Bamboo",
|
||||
"Bitbucket",
|
||||
"Bitrise",
|
||||
"GitHubActions",
|
||||
"GitLab",
|
||||
"Jenkins",
|
||||
"Rider",
|
||||
"SpaceAutomation",
|
||||
"TeamCity",
|
||||
"Terminal",
|
||||
"TravisCI",
|
||||
"VisualStudio",
|
||||
"VSCode"
|
||||
]
|
||||
},
|
||||
"IsDocsBuild": {
|
||||
"type": "boolean",
|
||||
"description": "Whether we are building for documentation - emits generated files"
|
||||
"$ref": "#/definitions/Host"
|
||||
},
|
||||
"NoLogo": {
|
||||
"type": "boolean",
|
||||
|
|
@ -74,65 +91,46 @@
|
|||
"type": "array",
|
||||
"description": "List of targets to be skipped. Empty list skips all dependencies",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"CI",
|
||||
"Clean",
|
||||
"Compile",
|
||||
"CompileCImGui",
|
||||
"CompileCImGuizmo",
|
||||
"CompileCImPlot",
|
||||
"CompileDalamud",
|
||||
"CompileDalamudBoot",
|
||||
"CompileDalamudCrashHandler",
|
||||
"CompileImGuiNatives",
|
||||
"CompileInjector",
|
||||
"CompileInjectorBoot",
|
||||
"Restore",
|
||||
"SetCILogging",
|
||||
"Test"
|
||||
]
|
||||
"$ref": "#/definitions/ExecutableTarget"
|
||||
}
|
||||
},
|
||||
"Solution": {
|
||||
"type": "string",
|
||||
"description": "Path to a solution file that is automatically loaded"
|
||||
},
|
||||
"Target": {
|
||||
"type": "array",
|
||||
"description": "List of targets to be invoked. Default is '{default_target}'",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"CI",
|
||||
"Clean",
|
||||
"Compile",
|
||||
"CompileCImGui",
|
||||
"CompileCImGuizmo",
|
||||
"CompileCImPlot",
|
||||
"CompileDalamud",
|
||||
"CompileDalamudBoot",
|
||||
"CompileDalamudCrashHandler",
|
||||
"CompileImGuiNatives",
|
||||
"CompileInjector",
|
||||
"CompileInjectorBoot",
|
||||
"Restore",
|
||||
"SetCILogging",
|
||||
"Test"
|
||||
]
|
||||
"$ref": "#/definitions/ExecutableTarget"
|
||||
}
|
||||
},
|
||||
"Verbosity": {
|
||||
"type": "string",
|
||||
"description": "Logging verbosity during build execution. Default is 'Normal'",
|
||||
"enum": [
|
||||
"Minimal",
|
||||
"Normal",
|
||||
"Quiet",
|
||||
"Verbose"
|
||||
]
|
||||
"$ref": "#/definitions/Verbosity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"Configuration": {
|
||||
"type": "string",
|
||||
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
|
||||
"enum": [
|
||||
"Debug",
|
||||
"Release"
|
||||
]
|
||||
},
|
||||
"IsDocsBuild": {
|
||||
"type": "boolean",
|
||||
"description": "Whether we are building for documentation - emits generated files"
|
||||
},
|
||||
"Solution": {
|
||||
"type": "string",
|
||||
"description": "Path to a solution file that is automatically loaded"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/NukeBuild"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,38 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
|||
|
||||
RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// String Table
|
||||
//
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
IDS_APPNAME "Dalamud Boot"
|
||||
IDS_MSVCRT_ACTION_OPENDOWNLOAD
|
||||
"Download Microsoft Visual C++ Redistributable 2022\nExit the game and download the latest setup file from Microsoft."
|
||||
IDS_MSVCRT_ACTION_IGNORE
|
||||
"Ignore and Continue\nAttempt to continue with the currently installed version.\nDalamud or plugins may fail to load."
|
||||
IDS_MSVCRT_DIALOG_MAININSTRUCTION
|
||||
"Outdated Microsoft Visual C++ Redistributable"
|
||||
IDS_MSVCRT_DIALOG_CONTENT
|
||||
"The Microsoft Visual C++ Redistributable version detected on this computer (v{0}.{1}.{2}.{3}) is out of date and may not work with Dalamud."
|
||||
IDS_MSVCRT_DOWNLOADURL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||
IDS_INITIALIZEFAIL_ACTION_ABORT "Abort\nExit the game."
|
||||
IDS_INITIALIZEFAIL_ACTION_CONTINUE
|
||||
"Load game without Dalamud\nThe game will launch without Dalamud enabled."
|
||||
IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION "Failed to load Dalamud."
|
||||
IDS_INITIALIZEFAIL_DIALOG_CONTENT
|
||||
"An error is preventing Dalamud from being loaded along with the game."
|
||||
END
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
IDS_INITIALIZEFAIL_DIALOG_FOOTER
|
||||
"Last operation: {0}\nHRESULT: 0x{1:08X}\nDescription: {2}"
|
||||
END
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<LanguageStandard>stdcpp23</LanguageStandard>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>false</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
|
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">26812</DisableSpecificWarnings>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
|
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|x64'">26812</DisableSpecificWarnings>
|
||||
|
|
@ -133,6 +133,10 @@
|
|||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="DalamudStartInfo.cpp" />
|
||||
<ClCompile Include="error_info.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="hooks.cpp" />
|
||||
<ClCompile Include="logging.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
|
|
@ -176,6 +180,7 @@
|
|||
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\trampoline.h" />
|
||||
<ClInclude Include="crashhandler_shared.h" />
|
||||
<ClInclude Include="DalamudStartInfo.h" />
|
||||
<ClInclude Include="error_info.h" />
|
||||
<ClInclude Include="hooks.h" />
|
||||
<ClInclude Include="logging.h" />
|
||||
<ClInclude Include="ntdll.h" />
|
||||
|
|
@ -206,4 +211,4 @@
|
|||
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\bin\$(Configuration)\" />
|
||||
<Copy SourceFiles="$(OutDir)nethost.dll" DestinationFolder="..\bin\$(Configuration)\" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
@ -76,6 +76,9 @@
|
|||
<ClCompile Include="ntdll.cpp">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="error_info.cpp">
|
||||
<Filter>Common Boot</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
|
||||
|
|
@ -146,6 +149,9 @@
|
|||
<ClInclude Include="ntdll.h">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="error_info.h">
|
||||
<Filter>Common Boot</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Dalamud.Boot.rc" />
|
||||
|
|
|
|||
|
|
@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
|||
config.LogName = json.value("LogName", config.LogName);
|
||||
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
|
||||
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
|
||||
|
||||
if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) {
|
||||
config.TempDirectory = json.value("TempDirectory", config.TempDirectory);
|
||||
}
|
||||
|
||||
config.Language = json.value("Language", config.Language);
|
||||
config.Platform = json.value("Platform", config.Platform);
|
||||
config.GameVersion = json.value("GameVersion", config.GameVersion);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ struct DalamudStartInfo {
|
|||
std::string ConfigurationPath;
|
||||
std::string LogPath;
|
||||
std::string LogName;
|
||||
std::string TempDirectory;
|
||||
std::string PluginDirectory;
|
||||
std::string AssetDirectory;
|
||||
ClientLanguage Language = ClientLanguage::English;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
|
||||
|
||||
struct exception_info
|
||||
{
|
||||
LPEXCEPTION_POINTERS pExceptionPointers;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
#include "utils.h"
|
||||
#include "veh.h"
|
||||
#include "xivfixes.h"
|
||||
#include "resource.h"
|
||||
|
||||
HMODULE g_hModule;
|
||||
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
|
||||
|
||||
void CheckMsvcrtVersion() {
|
||||
static void CheckMsvcrtVersion() {
|
||||
// Commit introducing inline mutex ctor: tagged vs-2022-17.14 (2024-06-18)
|
||||
// - https://github.com/microsoft/STL/commit/22a88260db4d754bbc067e2002430144d6ec5391
|
||||
// MSVC Redist versions:
|
||||
|
|
@ -28,67 +29,102 @@ void CheckMsvcrtVersion() {
|
|||
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[2]) << 16)
|
||||
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[3]) << 0);
|
||||
|
||||
#ifdef _DEBUG
|
||||
constexpr const wchar_t* RuntimeDllNames[] = {
|
||||
#ifdef _DEBUG
|
||||
L"msvcp140d.dll",
|
||||
L"vcruntime140d.dll",
|
||||
L"vcruntime140_1d.dll",
|
||||
};
|
||||
#else
|
||||
constexpr const wchar_t* RuntimeDllNames[] = {
|
||||
L"msvcp140.dll",
|
||||
L"vcruntime140.dll",
|
||||
L"vcruntime140_1.dll",
|
||||
};
|
||||
#endif
|
||||
};
|
||||
|
||||
uint64_t lowestVersion = 0;
|
||||
for (const auto& runtimeDllName : RuntimeDllNames) {
|
||||
const utils::loaded_module mod(GetModuleHandleW(runtimeDllName));
|
||||
if (!mod) {
|
||||
logging::E("Runtime DLL not found: {}", runtimeDllName);
|
||||
logging::E("MSVCRT DLL not found: {}", runtimeDllName);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto& versionFull = mod.get_file_version();
|
||||
logging::I("Runtime DLL {} has version {}.", runtimeDllName, utils::format_file_version(versionFull));
|
||||
const auto path = mod.path()
|
||||
.transform([](const auto& p) { return p.wstring(); })
|
||||
.value_or(runtimeDllName);
|
||||
|
||||
const auto version = (static_cast<uint64_t>(versionFull.dwFileVersionMS) << 32) |
|
||||
static_cast<uint64_t>(versionFull.dwFileVersionLS);
|
||||
if (const auto versionResult = mod.get_file_version()) {
|
||||
const auto& versionFull = versionResult->get();
|
||||
logging::I("MSVCRT DLL {} has version {}.", path, utils::format_file_version(versionFull));
|
||||
|
||||
const auto version = 0ULL |
|
||||
(static_cast<uint64_t>(versionFull.dwFileVersionMS) << 32) |
|
||||
(static_cast<uint64_t>(versionFull.dwFileVersionLS) << 0);
|
||||
|
||||
if (version < RequiredMsvcrtVersion && (lowestVersion == 0 || lowestVersion > version))
|
||||
lowestVersion = version;
|
||||
} catch (const std::exception& e) {
|
||||
logging::E("Failed to detect Runtime DLL version for {}: {}", runtimeDllName, e.what());
|
||||
} else {
|
||||
logging::E("Failed to detect MSVCRT DLL version for {}: {}", path, versionResult.error().describe());
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestVersion) {
|
||||
switch (MessageBoxW(
|
||||
nullptr,
|
||||
L"Microsoft Visual C++ Redistributable should be updated, or Dalamud may not work as expected."
|
||||
L" Do you want to download and install the latest version from Microsoft?"
|
||||
L"\n"
|
||||
L"\n* Clicking \"Yes\" will exit the game and open the download page from Microsoft."
|
||||
L"\n* Clicking \"No\" will continue loading the game with Dalamud. This may fail."
|
||||
L"\n"
|
||||
L"\nClick \"X64\" from the table in the download page, regardless of what CPU you have.",
|
||||
L"Dalamud",
|
||||
MB_YESNO | MB_ICONWARNING)) {
|
||||
case IDYES:
|
||||
ShellExecuteW(
|
||||
nullptr,
|
||||
L"open",
|
||||
L"https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version",
|
||||
nullptr,
|
||||
nullptr,
|
||||
SW_SHOW);
|
||||
ExitProcess(0);
|
||||
break;
|
||||
case IDNO:
|
||||
break;
|
||||
}
|
||||
if (!lowestVersion)
|
||||
return;
|
||||
|
||||
enum IdTaskDialogAction {
|
||||
IdTaskDialogActionOpenDownload = 101,
|
||||
IdTaskDialogActionIgnore,
|
||||
};
|
||||
|
||||
const TASKDIALOG_BUTTON buttons[]{
|
||||
{IdTaskDialogActionOpenDownload, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_OPENDOWNLOAD)},
|
||||
{IdTaskDialogActionIgnore, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_IGNORE)},
|
||||
};
|
||||
|
||||
const WORD lowestVersionComponents[]{
|
||||
static_cast<WORD>(lowestVersion >> 48),
|
||||
static_cast<WORD>(lowestVersion >> 32),
|
||||
static_cast<WORD>(lowestVersion >> 16),
|
||||
static_cast<WORD>(lowestVersion >> 0),
|
||||
};
|
||||
|
||||
const auto dialogContent = std::vformat(
|
||||
utils::get_string_resource(IDS_MSVCRT_DIALOG_CONTENT),
|
||||
std::make_wformat_args(
|
||||
lowestVersionComponents[0],
|
||||
lowestVersionComponents[1],
|
||||
lowestVersionComponents[2],
|
||||
lowestVersionComponents[3]));
|
||||
|
||||
const TASKDIALOGCONFIG config{
|
||||
.cbSize = sizeof config,
|
||||
.hInstance = g_hModule,
|
||||
.dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS,
|
||||
.pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
|
||||
.pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
|
||||
.pszMainInstruction = MAKEINTRESOURCEW(IDS_MSVCRT_DIALOG_MAININSTRUCTION),
|
||||
.pszContent = dialogContent.c_str(),
|
||||
.cButtons = _countof(buttons),
|
||||
.pButtons = buttons,
|
||||
.nDefaultButton = IdTaskDialogActionOpenDownload,
|
||||
};
|
||||
|
||||
int buttonPressed;
|
||||
if (utils::scoped_dpi_awareness_context ctx;
|
||||
FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
|
||||
buttonPressed = IdTaskDialogActionOpenDownload;
|
||||
|
||||
switch (buttonPressed) {
|
||||
case IdTaskDialogActionOpenDownload:
|
||||
ShellExecuteW(
|
||||
nullptr,
|
||||
L"open",
|
||||
utils::get_string_resource(IDS_MSVCRT_DOWNLOADURL).c_str(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
SW_SHOW);
|
||||
ExitProcess(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +139,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
}
|
||||
|
||||
if (g_startInfo.BootShowConsole)
|
||||
ConsoleSetup(L"Dalamud Boot");
|
||||
ConsoleSetup(utils::get_string_resource(IDS_APPNAME).c_str());
|
||||
|
||||
logging::update_dll_load_status(true);
|
||||
|
||||
|
|
@ -240,7 +276,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
|
||||
if (minHookLoaded) {
|
||||
logging::I("Applying fixes...");
|
||||
xivfixes::apply_all(true);
|
||||
std::thread([] { xivfixes::apply_all(true); }).join();
|
||||
logging::I("Fixes OK");
|
||||
} else {
|
||||
logging::W("Skipping fixes, as MinHook has failed to load.");
|
||||
|
|
@ -251,11 +287,14 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
while (!IsDebuggerPresent())
|
||||
Sleep(100);
|
||||
logging::I("Debugger attached.");
|
||||
__debugbreak();
|
||||
}
|
||||
|
||||
const auto fs_module_path = utils::get_module_path(g_hModule);
|
||||
const auto runtimeconfig_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
|
||||
const auto module_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.dll").wstring();
|
||||
const auto fs_module_path = utils::loaded_module(g_hModule).path();
|
||||
if (!fs_module_path)
|
||||
return fs_module_path.error();
|
||||
const auto runtimeconfig_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
|
||||
const auto module_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.dll").wstring();
|
||||
|
||||
// ============================== CLR ========================================= //
|
||||
|
||||
|
|
@ -292,6 +331,51 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
logging::I("VEH was disabled manually");
|
||||
}
|
||||
|
||||
// ============================== CLR Reporting =================================== //
|
||||
|
||||
// This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
|
||||
// was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
|
||||
// Ideally all of this will go away once they get to it.
|
||||
static std::shared_ptr<hooks::global_import_hook<decltype(ReportEventW)>> s_report_event_hook;
|
||||
s_report_event_hook = std::make_shared<hooks::global_import_hook<decltype(ReportEventW)>>(
|
||||
"advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
|
||||
s_report_event_hook->set_detour([hook = s_report_event_hook.get()](
|
||||
HANDLE hEventLog,
|
||||
WORD wType,
|
||||
WORD wCategory,
|
||||
DWORD dwEventID,
|
||||
PSID lpUserSid,
|
||||
WORD wNumStrings,
|
||||
DWORD dwDataSize,
|
||||
LPCWSTR* lpStrings,
|
||||
LPVOID lpRawData)-> BOOL {
|
||||
|
||||
// Check for CLR Error Event IDs
|
||||
// https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370
|
||||
if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
|
||||
dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
|
||||
dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
|
||||
dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow
|
||||
dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed
|
||||
{
|
||||
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
|
||||
}
|
||||
|
||||
if (wNumStrings == 0 || lpStrings == nullptr) {
|
||||
logging::W("ReportEventW called with no strings.");
|
||||
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
|
||||
}
|
||||
|
||||
// In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log.
|
||||
const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
|
||||
|
||||
const std::wstring error_details(lpStrings[0]);
|
||||
veh::raise_external_event(error_details);
|
||||
|
||||
return original_ret;
|
||||
});
|
||||
logging::I("ReportEventW hook installed.");
|
||||
|
||||
// ============================== Dalamud ==================================== //
|
||||
|
||||
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))
|
||||
|
|
|
|||
26
Dalamud.Boot/error_info.cpp
Normal file
26
Dalamud.Boot/error_info.cpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#include "error_info.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept
|
||||
: m_dalamudErrorDescription(dalamudErrorDescription)
|
||||
, m_hresult(hresult) {
|
||||
}
|
||||
|
||||
DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept
|
||||
: DalamudBootError(dalamudErrorDescription, E_FAIL) {
|
||||
}
|
||||
|
||||
const char* DalamudBootError::describe() const {
|
||||
switch (m_dalamudErrorDescription) {
|
||||
case DalamudBootErrorDescription::ModuleResourceLoadFail:
|
||||
return "Failed to load resource.";
|
||||
case DalamudBootErrorDescription::ModuleResourceVersionReadFail:
|
||||
return "Failed to query version information.";
|
||||
case DalamudBootErrorDescription::ModuleResourceVersionSignatureFail:
|
||||
return "Invalid version info found.";
|
||||
default:
|
||||
return "(unavailable)";
|
||||
}
|
||||
}
|
||||
42
Dalamud.Boot/error_info.h
Normal file
42
Dalamud.Boot/error_info.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
typedef unsigned long DWORD;
|
||||
typedef _Return_type_success_(return >= 0) long HRESULT;
|
||||
|
||||
enum class DalamudBootErrorDescription {
|
||||
None,
|
||||
ModulePathResolutionFail,
|
||||
ModuleResourceLoadFail,
|
||||
ModuleResourceVersionReadFail,
|
||||
ModuleResourceVersionSignatureFail,
|
||||
};
|
||||
|
||||
class DalamudBootError {
|
||||
DalamudBootErrorDescription m_dalamudErrorDescription;
|
||||
long m_hresult;
|
||||
|
||||
public:
|
||||
DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept;
|
||||
DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept;
|
||||
|
||||
const char* describe() const;
|
||||
|
||||
operator HRESULT() const {
|
||||
return m_hresult;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
using DalamudExpected = std::expected<
|
||||
std::conditional_t<
|
||||
std::is_reference_v<T>,
|
||||
std::reference_wrapper<std::remove_reference_t<T>>,
|
||||
T
|
||||
>,
|
||||
DalamudBootError
|
||||
>;
|
||||
|
||||
using DalamudUnexpected = std::unexpected<DalamudBootError>;
|
||||
|
|
@ -84,19 +84,13 @@ void hooks::getprocaddress_singleton_import_hook::initialize() {
|
|||
const auto dllName = unicode::convert<std::string>(pData->Loaded.FullDllName->Buffer);
|
||||
|
||||
utils::loaded_module mod(pData->Loaded.DllBase);
|
||||
std::wstring version, description;
|
||||
try {
|
||||
version = utils::format_file_version(mod.get_file_version());
|
||||
} catch (...) {
|
||||
version = L"<unknown>";
|
||||
}
|
||||
|
||||
try {
|
||||
description = mod.get_description();
|
||||
} catch (...) {
|
||||
description = L"<unknown>";
|
||||
}
|
||||
|
||||
const auto version = mod.get_file_version()
|
||||
.transform([](const auto& v) { return utils::format_file_version(v.get()); })
|
||||
.value_or(L"<unknown>");
|
||||
|
||||
const auto description = mod.get_description()
|
||||
.value_or(L"<unknown>");
|
||||
|
||||
logging::I(R"({} "{}" ("{}" ver {}) has been loaded at 0x{:X} ~ 0x{:X} (0x{:X}); finding import table items to hook.)",
|
||||
LogTag, dllName, description, version,
|
||||
reinterpret_cast<size_t>(pData->Loaded.DllBase),
|
||||
|
|
@ -125,7 +119,9 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
|
|||
if (mod.is_current_process())
|
||||
return;
|
||||
|
||||
const auto path = unicode::convert<std::string>(mod.path().wstring());
|
||||
const auto path = mod.path()
|
||||
.transform([](const auto& p) { return unicode::convert<std::string>(p.wstring()); })
|
||||
.value_or("<unknown>");
|
||||
|
||||
for (const auto& [hModule, targetFns] : m_targetFns) {
|
||||
for (const auto& [targetFn, pfnThunk] : targetFns) {
|
||||
|
|
@ -133,7 +129,7 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
|
|||
if (void* pGetProcAddressImport; mod.find_imported_function_pointer(dllName.c_str(), targetFn.c_str(), 0, pGetProcAddressImport)) {
|
||||
auto& hook = m_hooks[hModule][targetFn][mod];
|
||||
if (!hook) {
|
||||
logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, unicode::convert<std::string>(mod.path().wstring()));
|
||||
logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, path);
|
||||
|
||||
hook.emplace(std::format("getprocaddress_singleton_import_hook::hook_module({}!{})", dllName, targetFn), static_cast<void**>(pGetProcAddressImport), pfnThunk);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
#include <set>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
// https://www.akenotsuki.com/misc/srell/en/
|
||||
|
|
|
|||
|
|
@ -3,12 +3,23 @@
|
|||
// Used by Dalamud.Boot.rc
|
||||
//
|
||||
#define IDI_ICON1 101
|
||||
#define IDS_APPNAME 102
|
||||
#define IDS_MSVCRT_ACTION_OPENDOWNLOAD 103
|
||||
#define IDS_MSVCRT_ACTION_IGNORE 104
|
||||
#define IDS_MSVCRT_DIALOG_MAININSTRUCTION 105
|
||||
#define IDS_MSVCRT_DIALOG_CONTENT 106
|
||||
#define IDS_MSVCRT_DOWNLOADURL 107
|
||||
#define IDS_INITIALIZEFAIL_ACTION_ABORT 108
|
||||
#define IDS_INITIALIZEFAIL_ACTION_CONTINUE 109
|
||||
#define IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION 110
|
||||
#define IDS_INITIALIZEFAIL_DIALOG_CONTENT 111
|
||||
#define IDS_INITIALIZEFAIL_DIALOG_FOOTER 112
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||
#define _APS_NEXT_RESOURCE_VALUE 103
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "logging.h"
|
||||
#include "utils.h"
|
||||
#include "resource.h"
|
||||
|
||||
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
|
||||
|
||||
|
|
@ -379,12 +380,50 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara
|
|||
auto desc = err.Description();
|
||||
if (desc.length() == 0)
|
||||
desc = err.ErrorMessage();
|
||||
if (MessageBoxW(nullptr, std::format(
|
||||
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}",
|
||||
last_operation,
|
||||
desc.GetBSTR()).c_str(),
|
||||
L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
|
||||
ExitProcess(-1);
|
||||
|
||||
enum IdTaskDialogAction {
|
||||
IdTaskDialogActionAbort = 101,
|
||||
IdTaskDialogActionContinue,
|
||||
};
|
||||
|
||||
const TASKDIALOG_BUTTON buttons[]{
|
||||
{IdTaskDialogActionAbort, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_ABORT)},
|
||||
{IdTaskDialogActionContinue, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_CONTINUE)},
|
||||
};
|
||||
|
||||
const auto hru32 = static_cast<uint32_t>(hr);
|
||||
const auto footer = std::vformat(
|
||||
utils::get_string_resource(IDS_INITIALIZEFAIL_DIALOG_FOOTER),
|
||||
std::make_wformat_args(
|
||||
last_operation,
|
||||
hru32,
|
||||
desc.GetBSTR()));
|
||||
|
||||
const TASKDIALOGCONFIG config{
|
||||
.cbSize = sizeof config,
|
||||
.hInstance = g_hModule,
|
||||
.dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_EXPAND_FOOTER_AREA,
|
||||
.pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
|
||||
.pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
|
||||
.pszMainInstruction = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION),
|
||||
.pszContent = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_CONTENT),
|
||||
.cButtons = _countof(buttons),
|
||||
.pButtons = buttons,
|
||||
.nDefaultButton = IdTaskDialogActionAbort,
|
||||
.pszFooter = footer.c_str(),
|
||||
};
|
||||
|
||||
int buttonPressed;
|
||||
if (utils::scoped_dpi_awareness_context ctx;
|
||||
FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
|
||||
buttonPressed = IdTaskDialogActionAbort;
|
||||
|
||||
switch (buttonPressed) {
|
||||
case IdTaskDialogActionAbort:
|
||||
ExitProcess(-1);
|
||||
break;
|
||||
}
|
||||
|
||||
if (hMainThreadContinue) {
|
||||
CloseHandle(hMainThreadContinue);
|
||||
hMainThreadContinue = nullptr;
|
||||
|
|
|
|||
|
|
@ -3,22 +3,27 @@
|
|||
|
||||
#include "utils.h"
|
||||
|
||||
std::filesystem::path utils::loaded_module::path() const {
|
||||
std::wstring buf(MAX_PATH, L'\0');
|
||||
for (;;) {
|
||||
if (const auto len = GetModuleFileNameExW(GetCurrentProcess(), m_hModule, &buf[0], static_cast<DWORD>(buf.size())); len != buf.size()) {
|
||||
if (buf.empty())
|
||||
throw std::runtime_error(std::format("Failed to resolve module path: Win32 error {}", GetLastError()));
|
||||
DalamudExpected<std::filesystem::path> utils::loaded_module::path() const {
|
||||
for (std::wstring buf(MAX_PATH, L'\0');; buf.resize(buf.size() * 2)) {
|
||||
if (const auto len = GetModuleFileNameW(m_hModule, &buf[0], static_cast<DWORD>(buf.size()));
|
||||
len != buf.size()) {
|
||||
if (!len) {
|
||||
return DalamudUnexpected(
|
||||
std::in_place,
|
||||
DalamudBootErrorDescription::ModulePathResolutionFail,
|
||||
HRESULT_FROM_WIN32(GetLastError()));
|
||||
}
|
||||
|
||||
buf.resize(len);
|
||||
return buf;
|
||||
}
|
||||
|
||||
if (buf.size() * 2 < PATHCCH_MAX_CCH)
|
||||
buf.resize(buf.size() * 2);
|
||||
else if (auto p = std::filesystem::path(buf); exists(p))
|
||||
return p;
|
||||
else
|
||||
throw std::runtime_error("Failed to resolve module path: no amount of buffer size would fit the data");
|
||||
if (buf.size() > PATHCCH_MAX_CCH) {
|
||||
return DalamudUnexpected(
|
||||
std::in_place,
|
||||
DalamudBootErrorDescription::ModulePathResolutionFail,
|
||||
E_OUTOFMEMORY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,21 +149,24 @@ void* utils::loaded_module::get_imported_function_pointer(const char* pcszDllNam
|
|||
throw std::runtime_error(std::format("Failed to find import for {}!{} ({}).", pcszDllName, pcszFunctionName ? pcszFunctionName : "<unnamed>", hintOrOrdinal));
|
||||
}
|
||||
|
||||
std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
|
||||
DalamudExpected<std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
|
||||
const auto hres = FindResourceW(m_hModule, lpName, lpType);
|
||||
if (!hres)
|
||||
throw std::runtime_error("No such resource");
|
||||
return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
|
||||
|
||||
const auto hRes = LoadResource(m_hModule, hres);
|
||||
if (!hRes)
|
||||
throw std::runtime_error("LoadResource failure");
|
||||
return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
|
||||
|
||||
return {hRes, &FreeResource};
|
||||
return std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>(hRes, &FreeResource);
|
||||
}
|
||||
|
||||
std::wstring utils::loaded_module::get_description() const {
|
||||
const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
|
||||
const auto pBlock = LockResource(rsrc.get());
|
||||
DalamudExpected<std::wstring> utils::loaded_module::get_description() const {
|
||||
auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
|
||||
if (!rsrc)
|
||||
return DalamudUnexpected(std::move(rsrc.error()));
|
||||
|
||||
const auto pBlock = LockResource(rsrc->get());
|
||||
|
||||
struct LANGANDCODEPAGE {
|
||||
WORD wLanguage;
|
||||
|
|
@ -166,44 +174,65 @@ std::wstring utils::loaded_module::get_description() const {
|
|||
} * lpTranslate;
|
||||
UINT cbTranslate;
|
||||
if (!VerQueryValueW(pBlock,
|
||||
TEXT("\\VarFileInfo\\Translation"),
|
||||
L"\\VarFileInfo\\Translation",
|
||||
reinterpret_cast<LPVOID*>(&lpTranslate),
|
||||
&cbTranslate)) {
|
||||
throw std::runtime_error("Invalid version information (1)");
|
||||
return DalamudUnexpected(
|
||||
std::in_place,
|
||||
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
|
||||
HRESULT_FROM_WIN32(GetLastError()));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < (cbTranslate / sizeof(LANGANDCODEPAGE)); i++) {
|
||||
wchar_t subblockNameBuf[64];
|
||||
*std::format_to_n(
|
||||
subblockNameBuf,
|
||||
_countof(subblockNameBuf),
|
||||
L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
|
||||
lpTranslate[i].wLanguage,
|
||||
lpTranslate[i].wCodePage).out = 0;;
|
||||
|
||||
wchar_t* buf = nullptr;
|
||||
UINT size = 0;
|
||||
if (!VerQueryValueW(pBlock,
|
||||
std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
|
||||
lpTranslate[i].wLanguage,
|
||||
lpTranslate[i].wCodePage).c_str(),
|
||||
reinterpret_cast<LPVOID*>(&buf),
|
||||
&size)) {
|
||||
if (!VerQueryValueW(pBlock, subblockNameBuf, reinterpret_cast<LPVOID*>(&buf), &size))
|
||||
continue;
|
||||
}
|
||||
|
||||
auto currName = std::wstring_view(buf, size);
|
||||
while (!currName.empty() && currName.back() == L'\0')
|
||||
currName = currName.substr(0, currName.size() - 1);
|
||||
if (const auto p = currName.find(L'\0'); p != std::string::npos)
|
||||
currName = currName.substr(0, p);
|
||||
if (currName.empty())
|
||||
continue;
|
||||
return std::wstring(currName);
|
||||
}
|
||||
|
||||
throw std::runtime_error("Invalid version information (2)");
|
||||
return DalamudUnexpected(
|
||||
std::in_place,
|
||||
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
|
||||
HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
||||
}
|
||||
|
||||
const VS_FIXEDFILEINFO& utils::loaded_module::get_file_version() const {
|
||||
const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
|
||||
const auto pBlock = LockResource(rsrc.get());
|
||||
std::expected<std::reference_wrapper<const VS_FIXEDFILEINFO>, DalamudBootError> utils::loaded_module::get_file_version() const {
|
||||
auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
|
||||
if (!rsrc)
|
||||
return DalamudUnexpected(std::move(rsrc.error()));
|
||||
|
||||
const auto pBlock = LockResource(rsrc->get());
|
||||
UINT size = 0;
|
||||
LPVOID lpBuffer = nullptr;
|
||||
if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size))
|
||||
throw std::runtime_error("Failed to query version information.");
|
||||
if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size)) {
|
||||
return std::unexpected<DalamudBootError>(
|
||||
std::in_place,
|
||||
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
|
||||
HRESULT_FROM_WIN32(GetLastError()));
|
||||
}
|
||||
|
||||
const VS_FIXEDFILEINFO& versionInfo = *static_cast<const VS_FIXEDFILEINFO*>(lpBuffer);
|
||||
if (versionInfo.dwSignature != 0xfeef04bd)
|
||||
throw std::runtime_error("Invalid version info found.");
|
||||
if (versionInfo.dwSignature != 0xfeef04bd) {
|
||||
return std::unexpected<DalamudBootError>(
|
||||
std::in_place,
|
||||
DalamudBootErrorDescription::ModuleResourceVersionSignatureFail);
|
||||
}
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
|
|
@ -589,17 +618,10 @@ bool utils::is_running_on_wine() {
|
|||
return g_startInfo.Platform != "WINDOWS";
|
||||
}
|
||||
|
||||
std::filesystem::path utils::get_module_path(HMODULE hModule) {
|
||||
std::wstring buf(MAX_PATH, L'\0');
|
||||
while (true) {
|
||||
if (const auto res = GetModuleFileNameW(hModule, &buf[0], static_cast<int>(buf.size())); !res)
|
||||
throw std::runtime_error(std::format("GetModuleFileName failure: 0x{:X}", GetLastError()));
|
||||
else if (res < buf.size()) {
|
||||
buf.resize(res);
|
||||
return buf;
|
||||
} else
|
||||
buf.resize(buf.size() * 2);
|
||||
}
|
||||
std::wstring utils::get_string_resource(uint32_t resId) {
|
||||
LPCWSTR pstr;
|
||||
const auto len = LoadStringW(g_hModule, resId, reinterpret_cast<LPWSTR>(&pstr), 0);
|
||||
return std::wstring(pstr, len);
|
||||
}
|
||||
|
||||
HWND utils::try_find_game_window() {
|
||||
|
|
@ -677,3 +699,22 @@ std::wstring utils::format_win32_error(DWORD err) {
|
|||
|
||||
return std::format(L"Win32 error ({}=0x{:X})", err, err);
|
||||
}
|
||||
|
||||
utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context()
|
||||
: scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
|
||||
}
|
||||
|
||||
utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT context) {
|
||||
const auto user32 = GetModuleHandleW(L"user32.dll");
|
||||
m_setThreadDpiAwarenessContext =
|
||||
user32
|
||||
? reinterpret_cast<decltype(&SetThreadDpiAwarenessContext)>(
|
||||
GetProcAddress(user32, "SetThreadDpiAwarenessContext"))
|
||||
: nullptr;
|
||||
m_old = m_setThreadDpiAwarenessContext ? m_setThreadDpiAwarenessContext(context) : DPI_AWARENESS_CONTEXT_UNAWARE;
|
||||
}
|
||||
|
||||
utils::scoped_dpi_awareness_context::~scoped_dpi_awareness_context() {
|
||||
if (m_setThreadDpiAwarenessContext)
|
||||
m_setThreadDpiAwarenessContext(m_old);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
|
|
@ -7,6 +8,7 @@
|
|||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "error_info.h"
|
||||
#include "unicode.h"
|
||||
|
||||
namespace utils {
|
||||
|
|
@ -18,7 +20,7 @@ namespace utils {
|
|||
loaded_module(void* hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
|
||||
loaded_module(size_t hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
|
||||
|
||||
std::filesystem::path path() const;
|
||||
DalamudExpected<std::filesystem::path> path() const;
|
||||
|
||||
bool is_current_process() const { return m_hModule == GetModuleHandleW(nullptr); }
|
||||
bool owns_address(const void* pAddress) const;
|
||||
|
|
@ -57,9 +59,9 @@ namespace utils {
|
|||
void* get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) const;
|
||||
template<typename TFn> TFn** get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) { return reinterpret_cast<TFn**>(get_imported_function_pointer(pcszDllName, pcszFunctionName, hintOrOrdinal)); }
|
||||
|
||||
[[nodiscard]] std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
|
||||
[[nodiscard]] std::wstring get_description() const;
|
||||
[[nodiscard]] const VS_FIXEDFILEINFO& get_file_version() const;
|
||||
[[nodiscard]] DalamudExpected<std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
|
||||
[[nodiscard]] DalamudExpected<std::wstring> get_description() const;
|
||||
[[nodiscard]] DalamudExpected<const VS_FIXEDFILEINFO&> get_file_version() const;
|
||||
|
||||
static loaded_module current_process();
|
||||
static std::vector<loaded_module> all_modules();
|
||||
|
|
@ -268,7 +270,7 @@ namespace utils {
|
|||
|
||||
bool is_running_on_wine();
|
||||
|
||||
std::filesystem::path get_module_path(HMODULE hModule);
|
||||
std::wstring get_string_resource(uint32_t resId);
|
||||
|
||||
/// @brief Find the game main window.
|
||||
/// @return Handle to the game main window, or nullptr if it doesn't exist (yet).
|
||||
|
|
@ -279,4 +281,18 @@ namespace utils {
|
|||
std::wstring escape_shell_arg(const std::wstring& arg);
|
||||
|
||||
std::wstring format_win32_error(DWORD err);
|
||||
|
||||
class scoped_dpi_awareness_context {
|
||||
DPI_AWARENESS_CONTEXT m_old;
|
||||
decltype(&SetThreadDpiAwarenessContext) m_setThreadDpiAwarenessContext;
|
||||
|
||||
public:
|
||||
scoped_dpi_awareness_context();
|
||||
scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT);
|
||||
~scoped_dpi_awareness_context();
|
||||
scoped_dpi_awareness_context(const scoped_dpi_awareness_context&) = delete;
|
||||
scoped_dpi_awareness_context(scoped_dpi_awareness_context&&) = delete;
|
||||
scoped_dpi_awareness_context& operator=(const scoped_dpi_awareness_context&) = delete;
|
||||
scoped_dpi_awareness_context& operator=(scoped_dpi_awareness_context&&) = delete;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ HANDLE g_crashhandler_process = nullptr;
|
|||
HANDLE g_crashhandler_event = nullptr;
|
||||
HANDLE g_crashhandler_pipe_write = nullptr;
|
||||
|
||||
wchar_t g_external_event_info[16384] = L"";
|
||||
|
||||
std::recursive_mutex g_exception_handler_mutex;
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> g_time_start;
|
||||
|
|
@ -102,9 +104,13 @@ bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address)
|
|||
return false;
|
||||
}
|
||||
|
||||
static void append_injector_launch_args(std::vector<std::wstring>& args)
|
||||
static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstring>& args)
|
||||
{
|
||||
args.emplace_back(L"--game=\"" + utils::loaded_module::current_process().path().wstring() + L"\"");
|
||||
if (auto path = utils::loaded_module::current_process().path())
|
||||
args.emplace_back(L"--game=\"" + path->wstring() + L"\"");
|
||||
else
|
||||
return DalamudUnexpected(std::in_place, std::move(path.error()));
|
||||
|
||||
switch (g_startInfo.DalamudLoadMethod) {
|
||||
case DalamudStartInfo::LoadMethod::Entrypoint:
|
||||
args.emplace_back(L"--mode=entrypoint");
|
||||
|
|
@ -118,6 +124,7 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
|
|||
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
|
||||
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert<std::wstring>(g_startInfo.TempDirectory) + L"\"");
|
||||
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
|
||||
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
|
||||
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
|
||||
|
|
@ -155,6 +162,8 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
|
|||
args.emplace_back(szArgList[i]);
|
||||
LocalFree(szArgList);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
LONG exception_handler(EXCEPTION_POINTERS* ex)
|
||||
|
|
@ -184,7 +193,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
|
|||
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
|
||||
|
||||
std::wstring stackTrace;
|
||||
if (!g_clr)
|
||||
if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
|
||||
{
|
||||
stackTrace = std::wstring(g_external_event_info);
|
||||
}
|
||||
else if (!g_clr)
|
||||
{
|
||||
stackTrace = L"(no CLR stack trace available)";
|
||||
}
|
||||
|
|
@ -245,6 +258,12 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
|
|||
|
||||
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
|
||||
{
|
||||
// special case for CLR exceptions, always trigger crash handler
|
||||
if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
|
||||
{
|
||||
return exception_handler(ex);
|
||||
}
|
||||
|
||||
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
|
||||
{
|
||||
// pass
|
||||
|
|
@ -262,7 +281,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
|
|||
|
||||
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
|
||||
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
return exception_handler(ex);
|
||||
|
|
@ -291,7 +310,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536))
|
||||
{
|
||||
hWritePipe.emplace(hWritePipeRaw, &CloseHandle);
|
||||
|
||||
|
||||
if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
|
||||
{
|
||||
hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle);
|
||||
|
|
@ -309,9 +328,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
}
|
||||
|
||||
// additional information
|
||||
STARTUPINFOEXW siex{};
|
||||
STARTUPINFOEXW siex{};
|
||||
PROCESS_INFORMATION pi{};
|
||||
|
||||
|
||||
siex.StartupInfo.cb = sizeof siex;
|
||||
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
|
||||
siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE;
|
||||
|
|
@ -358,11 +377,20 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
args.emplace_back(std::format(L"--process-handle={}", reinterpret_cast<size_t>(hInheritableCurrentProcess)));
|
||||
args.emplace_back(std::format(L"--exception-info-pipe-read-handle={}", reinterpret_cast<size_t>(hReadPipeInheritable->get())));
|
||||
args.emplace_back(std::format(L"--asset-directory={}", unicode::convert<std::wstring>(g_startInfo.AssetDirectory)));
|
||||
args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
|
||||
? utils::loaded_module(g_hModule).path().parent_path().wstring()
|
||||
: std::filesystem::path(unicode::convert<std::wstring>(g_startInfo.BootLogPath)).parent_path().wstring()));
|
||||
if (const auto path = utils::loaded_module(g_hModule).path()) {
|
||||
args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
|
||||
? path->parent_path().wstring()
|
||||
: std::filesystem::path(unicode::convert<std::wstring>(g_startInfo.BootLogPath)).parent_path().wstring()));
|
||||
} else {
|
||||
logging::W("Failed to read path of the Dalamud Boot module: {}", path.error().describe());
|
||||
return false;
|
||||
}
|
||||
|
||||
args.emplace_back(L"--");
|
||||
append_injector_launch_args(args);
|
||||
if (auto r = append_injector_launch_args(args); !r) {
|
||||
logging::W("Failed to generate injector launch args: {}", r.error().describe());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& arg : args)
|
||||
{
|
||||
|
|
@ -370,7 +398,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
argstr.push_back(L' ');
|
||||
}
|
||||
argstr.pop_back();
|
||||
|
||||
|
||||
if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr))
|
||||
{
|
||||
logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError());
|
||||
|
|
@ -385,7 +413,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
TRUE, // Set handle inheritance to FALSE
|
||||
EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W)
|
||||
nullptr, // Use parent's environment block
|
||||
nullptr, // Use parent's starting directory
|
||||
nullptr, // Use parent's starting directory
|
||||
&siex.StartupInfo, // Pointer to STARTUPINFO structure
|
||||
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
|
||||
))
|
||||
|
|
@ -401,7 +429,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
}
|
||||
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
|
||||
g_crashhandler_process = pi.hProcess;
|
||||
g_crashhandler_pipe_write = hWritePipe->release();
|
||||
logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId);
|
||||
|
|
@ -419,3 +447,16 @@ bool veh::remove_handler()
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void veh::raise_external_event(const std::wstring& info)
|
||||
{
|
||||
const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1);
|
||||
wcsncpy_s(g_external_event_info, info.c_str(), info_size);
|
||||
RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) void BootVehRaiseExternalEventW(LPCWSTR info)
|
||||
{
|
||||
const std::wstring info_wstr(info);
|
||||
veh::raise_external_event(info_wstr);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ namespace veh
|
|||
{
|
||||
bool add_handler(bool doFullDump, const std::string& workingDirectory);
|
||||
bool remove_handler();
|
||||
void raise_external_event(const std::wstring& info);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,6 @@
|
|||
#include "ntdll.h"
|
||||
#include "utils.h"
|
||||
|
||||
template<typename T>
|
||||
static std::span<T> assume_nonempty_span(std::span<T> t, const char* descr) {
|
||||
if (t.empty())
|
||||
throw std::runtime_error(std::format("Unexpected empty span found: {}", descr));
|
||||
return t;
|
||||
}
|
||||
void xivfixes::unhook_dll(bool bApply) {
|
||||
static const auto LogTag = "[xivfixes:unhook_dll]";
|
||||
static const auto LogTagW = L"[xivfixes:unhook_dll]";
|
||||
|
|
@ -23,77 +17,90 @@ void xivfixes::unhook_dll(bool bApply) {
|
|||
|
||||
const auto mods = utils::loaded_module::all_modules();
|
||||
|
||||
const auto test_module = [&](size_t i, const utils::loaded_module & mod) {
|
||||
std::filesystem::path path;
|
||||
try {
|
||||
path = mod.path();
|
||||
std::wstring version, description;
|
||||
try {
|
||||
version = utils::format_file_version(mod.get_file_version());
|
||||
} catch (...) {
|
||||
version = L"<unknown>";
|
||||
}
|
||||
|
||||
try {
|
||||
description = mod.get_description();
|
||||
} catch (...) {
|
||||
description = L"<unknown>";
|
||||
}
|
||||
|
||||
logging::I(R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))", LogTagW, i + 1, mods.size(), mod.address_int(), mod.address_int() + mod.image_size(), mod.image_size(), path.wstring(), description, version);
|
||||
} catch (const std::exception& e) {
|
||||
logging::W("{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}", LogTag, i + 1, mods.size(), mod.address_int(), e.what());
|
||||
for (size_t i = 0; i < mods.size(); i++) {
|
||||
const auto& mod = mods[i];
|
||||
const auto path = mod.path();
|
||||
if (!path) {
|
||||
logging::W(
|
||||
"{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}",
|
||||
LogTag,
|
||||
i + 1,
|
||||
mods.size(),
|
||||
mod.address_int(),
|
||||
path.error().describe());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto moduleName = unicode::convert<std::string>(path.filename().wstring());
|
||||
const auto version = mod.get_file_version()
|
||||
.transform([](const auto& v) { return utils::format_file_version(v.get()); })
|
||||
.value_or(L"<unknown>");
|
||||
|
||||
std::vector<char> buf;
|
||||
std::string formatBuf;
|
||||
const auto description = mod.get_description()
|
||||
.value_or(L"<unknown>");
|
||||
|
||||
logging::I(
|
||||
R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))",
|
||||
LogTagW,
|
||||
i + 1,
|
||||
mods.size(),
|
||||
mod.address_int(),
|
||||
mod.address_int() + mod.image_size(),
|
||||
mod.image_size(),
|
||||
path->wstring(),
|
||||
description,
|
||||
version);
|
||||
|
||||
const auto moduleName = unicode::convert<std::string>(path->filename().wstring());
|
||||
|
||||
const auto& sectionHeader = mod.section_header(".text");
|
||||
const auto section = mod.span_as<char>(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize);
|
||||
if (section.empty()) {
|
||||
logging::W("{} Error: .text[VA:VA + VS] is empty", LogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
auto hFsDllRaw = CreateFileW(path->c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (hFsDllRaw == INVALID_HANDLE_VALUE) {
|
||||
logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
auto hFsDll = std::unique_ptr<void, decltype(&CloseHandle)>(hFsDllRaw, &CloseHandle);
|
||||
std::vector<char> buf(section.size());
|
||||
SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
|
||||
if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast<DWORD>(buf.size()), &read, nullptr)) {
|
||||
if (read < section.size_bytes()) {
|
||||
logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert<std::string>(path->filename().u8string()));
|
||||
try {
|
||||
const auto& sectionHeader = mod.section_header(".text");
|
||||
const auto section = assume_nonempty_span(mod.span_as<char>(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize), ".text[VA:VA+VS]");
|
||||
auto hFsDllRaw = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (hFsDllRaw == INVALID_HANDLE_VALUE) {
|
||||
logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
|
||||
return;
|
||||
}
|
||||
auto hFsDll = std::unique_ptr<void, decltype(CloseHandle)*>(hFsDllRaw, &CloseHandle);
|
||||
|
||||
buf.resize(section.size());
|
||||
SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
|
||||
if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast<DWORD>(buf.size()), &read, nullptr)) {
|
||||
if (read < section.size_bytes()) {
|
||||
logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert<std::string>(path.filename().u8string()));
|
||||
|
||||
std::optional<utils::memory_tenderizer> tenderizer;
|
||||
for (size_t i = 0, instructionLength = 1, printed = 0; i < buf.size(); i += instructionLength) {
|
||||
if (section[i] == buf[i]) {
|
||||
std::string formatBuf;
|
||||
for (size_t inst = 0, instructionLength = 1, printed = 0; inst < buf.size(); inst += instructionLength) {
|
||||
if (section[inst] == buf[inst]) {
|
||||
instructionLength = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto rva = sectionHeader.VirtualAddress + i;
|
||||
const auto rva = sectionHeader.VirtualAddress + inst;
|
||||
nmd_x86_instruction instruction{};
|
||||
if (!nmd_x86_decode(§ion[i], section.size() - i, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
|
||||
if (!nmd_x86_decode(§ion[inst], section.size() - inst, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
|
||||
instructionLength = 1;
|
||||
if (printed < 64) {
|
||||
logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast<uint8_t>(section[i]));
|
||||
logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast<uint8_t>(section[inst]));
|
||||
printed++;
|
||||
}
|
||||
} else {
|
||||
instructionLength = instruction.length;
|
||||
if (printed < 64) {
|
||||
formatBuf.resize(128);
|
||||
nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast<size_t>(§ion[i]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
|
||||
nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast<size_t>(§ion[inst]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
|
||||
formatBuf.resize(strnlen(&formatBuf[0], formatBuf.size()));
|
||||
|
||||
const auto& directory = mod.data_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
|
||||
|
|
@ -103,25 +110,25 @@ void xivfixes::unhook_dll(bool bApply) {
|
|||
const auto functions = mod.span_as<DWORD>(exportDirectory.AddressOfFunctions, exportDirectory.NumberOfFunctions);
|
||||
|
||||
std::string resolvedExportName;
|
||||
for (size_t j = 0; j < names.size(); ++j) {
|
||||
for (size_t nameIndex = 0; nameIndex < names.size(); ++nameIndex) {
|
||||
std::string_view name;
|
||||
if (const char* pcszName = mod.address_as<char>(names[j]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
|
||||
if (const char* pcszName = mod.address_as<char>(names[nameIndex]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
|
||||
if (IsBadReadPtr(pcszName, 256)) {
|
||||
logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, j);
|
||||
logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, nameIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
name = std::string_view(pcszName, strnlen(pcszName, 256));
|
||||
logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, j, name);
|
||||
logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, nameIndex, name);
|
||||
}
|
||||
|
||||
if (ordinals[j] >= functions.size()) {
|
||||
logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, j, ordinals[j], functions.size());
|
||||
if (ordinals[nameIndex] >= functions.size()) {
|
||||
logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, nameIndex, ordinals[nameIndex], functions.size());
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto rva = functions[ordinals[j]];
|
||||
if (rva == §ion[i] - mod.address()) {
|
||||
const auto rva = functions[ordinals[nameIndex]];
|
||||
if (rva == §ion[inst] - mod.address()) {
|
||||
resolvedExportName = std::format("[export:{}]", name);
|
||||
break;
|
||||
}
|
||||
|
|
@ -135,7 +142,7 @@ void xivfixes::unhook_dll(bool bApply) {
|
|||
if (doRestore) {
|
||||
if (!tenderizer)
|
||||
tenderizer.emplace(section, PAGE_EXECUTE_READWRITE);
|
||||
memcpy(§ion[i], &buf[i], instructionLength);
|
||||
memcpy(§ion[inst], &buf[inst], instructionLength);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,21 +154,7 @@ void xivfixes::unhook_dll(bool bApply) {
|
|||
} catch (const std::exception& e) {
|
||||
logging::W("{} Error: {}", LogTag, e.what());
|
||||
}
|
||||
};
|
||||
|
||||
// This is needed since try and __try cannot be used in the same function. Lambdas circumvent the limitation.
|
||||
const auto windows_exception_handler = [&]() {
|
||||
for (size_t i = 0; i < mods.size(); i++) {
|
||||
const auto& mod = mods[i];
|
||||
__try {
|
||||
test_module(i, mod);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
logging::W("{} Error: Access Violation", LogTag);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
windows_exception_handler();
|
||||
}
|
||||
}
|
||||
|
||||
using TFnGetInputDeviceManager = void* ();
|
||||
|
|
@ -294,13 +287,11 @@ static bool is_xivalex(const std::filesystem::path& dllPath) {
|
|||
static bool is_openprocess_already_dealt_with() {
|
||||
static const auto s_value = [] {
|
||||
for (const auto& mod : utils::loaded_module::all_modules()) {
|
||||
try {
|
||||
if (is_xivalex(mod.path()))
|
||||
return true;
|
||||
|
||||
} catch (...) {
|
||||
// pass
|
||||
}
|
||||
const auto path = mod.path().value_or({});
|
||||
if (path.empty())
|
||||
continue;
|
||||
if (is_xivalex(path))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
|
@ -650,43 +641,22 @@ void xivfixes::symbol_load_patches(bool bApply) {
|
|||
|
||||
void xivfixes::disable_game_debugging_protection(bool bApply) {
|
||||
static const char* LogTag = "[xivfixes:disable_game_debugging_protection]";
|
||||
static const std::vector<uint8_t> patchBytes = {
|
||||
0x31, 0xC0, // XOR EAX, EAX
|
||||
0x90, // NOP
|
||||
0x90, // NOP
|
||||
0x90, // NOP
|
||||
0x90 // NOP
|
||||
};
|
||||
static std::optional<hooks::import_hook<decltype(IsDebuggerPresent)>> s_hookIsDebuggerPresent;
|
||||
|
||||
if (!bApply)
|
||||
return;
|
||||
if (bApply) {
|
||||
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
|
||||
logging::I("{} Turned off via environment variable.", LogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
|
||||
logging::I("{} Turned off via environment variable.", LogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find IsDebuggerPresent in Framework.Tick()
|
||||
const char* matchPtr = utils::signature_finder()
|
||||
.look_in(utils::loaded_module(g_hGameInstance), ".text")
|
||||
.look_for_hex("FF 15 ?? ?? ?? ?? 85 C0 74 13 41")
|
||||
.find_one()
|
||||
.Match.data();
|
||||
|
||||
if (!matchPtr) {
|
||||
logging::E("{} Failed to find signature.", LogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
void* address = const_cast<void*>(static_cast<const void*>(matchPtr));
|
||||
|
||||
DWORD oldProtect;
|
||||
if (VirtualProtect(address, patchBytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) {
|
||||
memcpy(address, patchBytes.data(), patchBytes.size());
|
||||
VirtualProtect(address, patchBytes.size(), oldProtect, &oldProtect);
|
||||
logging::I("{} Patch applied at address 0x{:X}.", LogTag, reinterpret_cast<uintptr_t>(address));
|
||||
s_hookIsDebuggerPresent.emplace("kernel32.dll!IsDebuggerPresent", "kernel32.dll", "IsDebuggerPresent", 0);
|
||||
s_hookIsDebuggerPresent->set_detour([]() { return false; });
|
||||
logging::I("{} Enable", LogTag);
|
||||
} else {
|
||||
logging::E("{} Failed to change memory protection.", LogTag);
|
||||
if (s_hookIsDebuggerPresent) {
|
||||
logging::I("{} Disable", LogTag);
|
||||
s_hookIsDebuggerPresent.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ public record DalamudStartInfo
|
|||
/// </summary>
|
||||
public string? ConfigurationPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user.
|
||||
/// It should also be predictable and easy for launchers to find.
|
||||
/// </summary>
|
||||
public string? TempDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path of the log files.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
|
||||
<PackageReference Include="Lumina" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{8874326B-E755-4D13-90B4-59AB263A3E6B}</ProjectGuid>
|
||||
<RootNamespace>Dalamud_Injector_Boot</RootNamespace>
|
||||
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<TargetName>Dalamud.Injector</TargetName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<OutDir>..\bin\$(Configuration)\</OutDir>
|
||||
<IntDir>obj\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<ProgramDatabaseFile>$(OutDir)$(TargetName).Boot.pdb</ProgramDatabaseFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>false</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>false</EnableCOMDATFolding>
|
||||
<OptimizeReferences>false</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ItemGroup>
|
||||
<Content Include="..\lib\CoreCLR\nethost\nethost.dll">
|
||||
<Link>nethost.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="dalamud.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="resources.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\Dalamud.Boot\logging.cpp" />
|
||||
<ClCompile Include="..\Dalamud.Boot\unicode.cpp" />
|
||||
<ClCompile Include="..\lib\CoreCLR\boot.cpp" />
|
||||
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\Dalamud.Boot\logging.h" />
|
||||
<ClInclude Include="..\Dalamud.Boot\unicode.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
|
||||
<Delete Files="$(OutDir)$(TargetName).lib" />
|
||||
<Delete Files="$(OutDir)$(TargetName).exp" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{4faac519-3a73-4b2b-96e7-fb597f02c0be}</UniqueIdentifier>
|
||||
<Extensions>ico;rc</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="dalamud.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="resources.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\lib\CoreCLR\boot.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Dalamud.Boot\logging.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Dalamud.Boot\unicode.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Dalamud.Boot\logging.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Dalamud.Boot\unicode.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
#define WIN32_LEAN_AND_MEAN
|
||||
|
||||
#include <filesystem>
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include "..\Dalamud.Boot\logging.h"
|
||||
#include "..\lib\CoreCLR\CoreCLR.h"
|
||||
#include "..\lib\CoreCLR\boot.h"
|
||||
|
||||
int wmain(int argc, wchar_t** argv)
|
||||
{
|
||||
// Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read
|
||||
//logging::start_file_logging("dalamud.injector.boot.log", false);
|
||||
logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors");
|
||||
logging::I("Built at : " __DATE__ "@" __TIME__);
|
||||
|
||||
wchar_t _module_path[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
|
||||
std::filesystem::path fs_module_path(_module_path);
|
||||
|
||||
std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
|
||||
std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
|
||||
|
||||
// =========================================================================== //
|
||||
|
||||
void* entrypoint_vfn;
|
||||
const auto result = InitializeClrAndGetEntryPoint(
|
||||
GetModuleHandleW(nullptr),
|
||||
false,
|
||||
runtimeconfig_path,
|
||||
module_path,
|
||||
L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
|
||||
L"Main",
|
||||
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
|
||||
&entrypoint_vfn);
|
||||
|
||||
if (FAILED(result))
|
||||
return result;
|
||||
|
||||
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
|
||||
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
|
||||
|
||||
logging::I("Running Dalamud Injector...");
|
||||
const auto ret = entrypoint_fn(argc, argv);
|
||||
logging::I("Done!");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
#pragma once
|
||||
|
|
@ -1 +0,0 @@
|
|||
MAINICON ICON "dalamud.ico"
|
||||
|
|
@ -13,12 +13,13 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Output">
|
||||
<OutputType>Library</OutputType>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputPath>..\bin\$(Configuration)\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<ApplicationIcon>dalamud.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Documentation">
|
||||
|
|
@ -52,17 +53,18 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Iced" Version="1.17.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="PeNet" Version="2.6.4" />
|
||||
<PackageReference Include="Reloaded.Memory" Version="7.0.0" />
|
||||
<PackageReference Include="Reloaded.Memory.Buffers" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
|
||||
<PackageReference Include="Iced" />
|
||||
<PackageReference Include="JetBrains.Annotations" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" />
|
||||
<PackageReference Include="PeNet" />
|
||||
<PackageReference Include="Reloaded.Memory" />
|
||||
<PackageReference Include="Reloaded.Memory.Buffers" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ using Reloaded.Memory.Buffers;
|
|||
using Reloaded.Memory.Sources;
|
||||
using Reloaded.Memory.Utilities;
|
||||
using Serilog;
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
using static Dalamud.Injector.NativeFunctions;
|
||||
using static Iced.Intel.AssemblerRegisters;
|
||||
|
||||
namespace Dalamud.Injector
|
||||
|
|
@ -88,7 +88,7 @@ namespace Dalamud.Injector
|
|||
if (lpParameter == 0)
|
||||
throw new Exception("Unable to allocate LoadLibraryW parameter");
|
||||
|
||||
this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter, out var err);
|
||||
var err = this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter);
|
||||
this.extMemory.Read<IntPtr>(this.loadLibraryRetPtr, out address);
|
||||
if (address == IntPtr.Zero)
|
||||
throw new Exception($"LoadLibraryW(\"{modulePath}\") failure: {new Win32Exception((int)err).Message} ({err})");
|
||||
|
|
@ -108,7 +108,7 @@ namespace Dalamud.Injector
|
|||
if (lpParameter == 0)
|
||||
throw new Exception("Unable to allocate GetProcAddress parameter ptr");
|
||||
|
||||
this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter, out var err);
|
||||
var err = this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter);
|
||||
this.extMemory.Read<nuint>(this.getProcAddressRetPtr, out address);
|
||||
if (address == 0)
|
||||
throw new Exception($"GetProcAddress(0x{module:X}, \"{functionName}\") failure: {new Win32Exception((int)err).Message} ({err})");
|
||||
|
|
@ -119,27 +119,30 @@ namespace Dalamud.Injector
|
|||
/// </summary>
|
||||
/// <param name="methodAddress">Method address.</param>
|
||||
/// <param name="parameterAddress">Parameter address.</param>
|
||||
/// <param name="exitCode">Thread exit code.</param>
|
||||
public void CallRemoteFunction(nuint methodAddress, nuint parameterAddress, out uint exitCode)
|
||||
/// <returns>Thread exit code.</returns>
|
||||
public unsafe uint CallRemoteFunction(nuint methodAddress, nuint parameterAddress)
|
||||
{
|
||||
// Create and initialize a thread at our address and parameter address.
|
||||
var threadHandle = CreateRemoteThread(
|
||||
this.targetProcess.Handle,
|
||||
IntPtr.Zero,
|
||||
var threadHandle = Windows.Win32.PInvoke.CreateRemoteThread(
|
||||
new HANDLE(this.targetProcess.Handle.ToPointer()),
|
||||
null,
|
||||
UIntPtr.Zero,
|
||||
methodAddress,
|
||||
parameterAddress,
|
||||
CreateThreadFlags.RunImmediately,
|
||||
out _);
|
||||
(delegate* unmanaged[Stdcall]<void*, uint>)methodAddress,
|
||||
parameterAddress.ToPointer(),
|
||||
0, // Run immediately
|
||||
null);
|
||||
|
||||
if (threadHandle == IntPtr.Zero)
|
||||
throw new Exception($"CreateRemoteThread failure: {Marshal.GetLastWin32Error()}");
|
||||
|
||||
_ = WaitForSingleObject(threadHandle, uint.MaxValue);
|
||||
_ = Windows.Win32.PInvoke.WaitForSingleObject(threadHandle, uint.MaxValue);
|
||||
|
||||
GetExitCodeThread(threadHandle, out exitCode);
|
||||
uint exitCode = 0;
|
||||
if (!Windows.Win32.PInvoke.GetExitCodeThread(threadHandle, &exitCode))
|
||||
throw new Exception($"GetExitCodeThread failure: {Marshal.GetLastWin32Error()}");
|
||||
|
||||
CloseHandle(threadHandle);
|
||||
Windows.Win32.PInvoke.CloseHandle(threadHandle);
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports)
|
||||
|
|
|
|||
|
|
@ -1,956 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Injector
|
||||
{
|
||||
/// <summary>
|
||||
/// Native user32 functions.
|
||||
/// </summary>
|
||||
internal static partial class NativeFunctions
|
||||
{
|
||||
/// <summary>
|
||||
/// MB_* from winuser.
|
||||
/// </summary>
|
||||
public enum MessageBoxType : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// The default value for any of the various subtypes.
|
||||
/// </summary>
|
||||
DefaultValue = 0x0,
|
||||
|
||||
// To indicate the buttons displayed in the message box, specify one of the following values.
|
||||
|
||||
/// <summary>
|
||||
/// The message box contains three push buttons: Abort, Retry, and Ignore.
|
||||
/// </summary>
|
||||
AbortRetryIgnore = 0x2,
|
||||
|
||||
/// <summary>
|
||||
/// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead
|
||||
/// of MB_ABORTRETRYIGNORE.
|
||||
/// </summary>
|
||||
CancelTryContinue = 0x6,
|
||||
|
||||
/// <summary>
|
||||
/// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends
|
||||
/// a WM_HELP message to the owner.
|
||||
/// </summary>
|
||||
Help = 0x4000,
|
||||
|
||||
/// <summary>
|
||||
/// The message box contains one push button: OK. This is the default.
|
||||
/// </summary>
|
||||
Ok = DefaultValue,
|
||||
|
||||
/// <summary>
|
||||
/// The message box contains two push buttons: OK and Cancel.
|
||||
/// </summary>
|
||||
OkCancel = 0x1,
|
||||
|
||||
/// <summary>
|
||||
/// The message box contains two push buttons: Retry and Cancel.
|
||||
/// </summary>
|
||||
RetryCancel = 0x5,
|
||||
|
||||
/// <summary>
|
||||
/// The message box contains two push buttons: Yes and No.
|
||||
/// </summary>
|
||||
YesNo = 0x4,
|
||||
|
||||
/// <summary>
|
||||
/// The message box contains three push buttons: Yes, No, and Cancel.
|
||||
/// </summary>
|
||||
YesNoCancel = 0x3,
|
||||
|
||||
// To display an icon in the message box, specify one of the following values.
|
||||
|
||||
/// <summary>
|
||||
/// An exclamation-point icon appears in the message box.
|
||||
/// </summary>
|
||||
IconExclamation = 0x30,
|
||||
|
||||
/// <summary>
|
||||
/// An exclamation-point icon appears in the message box.
|
||||
/// </summary>
|
||||
IconWarning = IconExclamation,
|
||||
|
||||
/// <summary>
|
||||
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
|
||||
/// </summary>
|
||||
IconInformation = 0x40,
|
||||
|
||||
/// <summary>
|
||||
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
|
||||
/// </summary>
|
||||
IconAsterisk = IconInformation,
|
||||
|
||||
/// <summary>
|
||||
/// A question-mark icon appears in the message box.
|
||||
/// The question-mark message icon is no longer recommended because it does not clearly represent a specific type
|
||||
/// of message and because the phrasing of a message as a question could apply to any message type. In addition,
|
||||
/// users can confuse the message symbol question mark with Help information. Therefore, do not use this question
|
||||
/// mark message symbol in your message boxes. The system continues to support its inclusion only for backward
|
||||
/// compatibility.
|
||||
/// </summary>
|
||||
IconQuestion = 0x20,
|
||||
|
||||
/// <summary>
|
||||
/// A stop-sign icon appears in the message box.
|
||||
/// </summary>
|
||||
IconStop = 0x10,
|
||||
|
||||
/// <summary>
|
||||
/// A stop-sign icon appears in the message box.
|
||||
/// </summary>
|
||||
IconError = IconStop,
|
||||
|
||||
/// <summary>
|
||||
/// A stop-sign icon appears in the message box.
|
||||
/// </summary>
|
||||
IconHand = IconStop,
|
||||
|
||||
// To indicate the default button, specify one of the following values.
|
||||
|
||||
/// <summary>
|
||||
/// The first button is the default button.
|
||||
/// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
|
||||
/// </summary>
|
||||
DefButton1 = DefaultValue,
|
||||
|
||||
/// <summary>
|
||||
/// The second button is the default button.
|
||||
/// </summary>
|
||||
DefButton2 = 0x100,
|
||||
|
||||
/// <summary>
|
||||
/// The third button is the default button.
|
||||
/// </summary>
|
||||
DefButton3 = 0x200,
|
||||
|
||||
/// <summary>
|
||||
/// The fourth button is the default button.
|
||||
/// </summary>
|
||||
DefButton4 = 0x300,
|
||||
|
||||
// To indicate the modality of the dialog box, specify one of the following values.
|
||||
|
||||
/// <summary>
|
||||
/// The user must respond to the message box before continuing work in the window identified by the hWnd parameter.
|
||||
/// However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy
|
||||
/// of windows in the application, the user may be able to move to other windows within the thread. All child windows
|
||||
/// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the
|
||||
/// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
|
||||
/// </summary>
|
||||
ApplModal = DefaultValue,
|
||||
|
||||
/// <summary>
|
||||
/// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style.
|
||||
/// Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate
|
||||
/// attention (for example, running out of memory). This flag has no effect on the user's ability to interact with
|
||||
/// windows other than those associated with hWnd.
|
||||
/// </summary>
|
||||
SystemModal = 0x1000,
|
||||
|
||||
/// <summary>
|
||||
/// Same as MB_APPLMODAL except that all the top-level windows belonging to the current thread are disabled if the
|
||||
/// hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle
|
||||
/// available but still needs to prevent input to other windows in the calling thread without suspending other threads.
|
||||
/// </summary>
|
||||
TaskModal = 0x2000,
|
||||
|
||||
// To specify other options, use one or more of the following values.
|
||||
|
||||
/// <summary>
|
||||
/// Same as desktop of the interactive window station. For more information, see Window Stations. If the current
|
||||
/// input desktop is not the default desktop, MessageBox does not return until the user switches to the default
|
||||
/// desktop.
|
||||
/// </summary>
|
||||
DefaultDesktopOnly = 0x20000,
|
||||
|
||||
/// <summary>
|
||||
/// The text is right-justified.
|
||||
/// </summary>
|
||||
Right = 0x80000,
|
||||
|
||||
/// <summary>
|
||||
/// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems.
|
||||
/// </summary>
|
||||
RtlReading = 0x100000,
|
||||
|
||||
/// <summary>
|
||||
/// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function
|
||||
/// for the message box.
|
||||
/// </summary>
|
||||
SetForeground = 0x10000,
|
||||
|
||||
/// <summary>
|
||||
/// The message box is created with the WS_EX_TOPMOST window style.
|
||||
/// </summary>
|
||||
Topmost = 0x40000,
|
||||
|
||||
/// <summary>
|
||||
/// The caller is a service notifying the user of an event. The function displays a message box on the current active
|
||||
/// desktop, even if there is no user logged on to the computer.
|
||||
/// </summary>
|
||||
ServiceNotification = 0x200000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message,
|
||||
/// such as status or error information. The message box returns an integer value that indicates which button the user
|
||||
/// clicked.
|
||||
/// </summary>
|
||||
/// <param name="hWnd">
|
||||
/// A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no
|
||||
/// owner window.
|
||||
/// </param>
|
||||
/// <param name="text">
|
||||
/// The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage
|
||||
/// return and/or linefeed character between each line.
|
||||
/// </param>
|
||||
/// <param name="caption">
|
||||
/// The dialog box title. If this parameter is NULL, the default title is Error.</param>
|
||||
/// <param name="type">
|
||||
/// The contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups
|
||||
/// of flags.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If a message box has a Cancel button, the function returns the IDCANCEL value if either the ESC key is pressed or
|
||||
/// the Cancel button is selected. If the message box has no Cancel button, pressing ESC will no effect - unless an
|
||||
/// MB_OK button is present. If an MB_OK button is displayed and the user presses ESC, the return value will be IDOK.
|
||||
/// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function
|
||||
/// succeeds, the return value is one of the ID* enum values.
|
||||
/// </returns>
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Native kernel32 functions.
|
||||
/// </summary>
|
||||
internal static partial class NativeFunctions
|
||||
{
|
||||
/// <summary>
|
||||
/// MEM_* from memoryapi.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum AllocationType
|
||||
{
|
||||
/// <summary>
|
||||
/// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce
|
||||
/// placeholders, lpAddress and dwSize must exactly match those of the placeholder.
|
||||
/// </summary>
|
||||
CoalescePlaceholders = 0x1,
|
||||
|
||||
/// <summary>
|
||||
/// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using
|
||||
/// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify
|
||||
/// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER.
|
||||
/// </summary>
|
||||
PreservePlaceholder = 0x2,
|
||||
|
||||
/// <summary>
|
||||
/// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved
|
||||
/// memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents
|
||||
/// will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed.
|
||||
/// To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Attempting to commit
|
||||
/// a specific address range by specifying MEM_COMMIT without MEM_RESERVE and a non-NULL lpAddress fails unless the
|
||||
/// entire range has already been reserved. The resulting error code is ERROR_INVALID_ADDRESS. An attempt to commit
|
||||
/// a page that is already committed does not cause the function to fail. This means that you can commit pages without
|
||||
/// first determining the current commitment state of each page. If lpAddress specifies an address within an enclave,
|
||||
/// flAllocationType must be MEM_COMMIT.
|
||||
/// </summary>
|
||||
Commit = 0x1000,
|
||||
|
||||
/// <summary>
|
||||
/// Reserves a range of the process's virtual address space without allocating any actual physical storage in memory
|
||||
/// or in the paging file on disk. You commit reserved pages by calling VirtualAllocEx again with MEM_COMMIT. To
|
||||
/// reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Other memory allocation
|
||||
/// functions, such as malloc and LocalAlloc, cannot use reserved memory until it has been released.
|
||||
/// </summary>
|
||||
Reserve = 0x2000,
|
||||
|
||||
/// <summary>
|
||||
/// Decommits the specified region of committed pages. After the operation, the pages are in the reserved state.
|
||||
/// The function does not fail if you attempt to decommit an uncommitted page. This means that you can decommit
|
||||
/// a range of pages without first determining the current commitment state. The MEM_DECOMMIT value is not supported
|
||||
/// when the lpAddress parameter provides the base address for an enclave.
|
||||
/// </summary>
|
||||
Decommit = 0x4000,
|
||||
|
||||
/// <summary>
|
||||
/// Releases the specified region of pages, or placeholder (for a placeholder, the address space is released and
|
||||
/// available for other allocations). After this operation, the pages are in the free state. If you specify this
|
||||
/// value, dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function
|
||||
/// when the region is reserved. The function fails if either of these conditions is not met. If any pages in the
|
||||
/// region are committed currently, the function first decommits, and then releases them. The function does not
|
||||
/// fail if you attempt to release pages that are in different states, some reserved and some committed. This means
|
||||
/// that you can release a range of pages without first determining the current commitment state.
|
||||
/// </summary>
|
||||
Release = 0x8000,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that data in the memory range specified by lpAddress and dwSize is no longer of interest. The pages
|
||||
/// should not be read from or written to the paging file. However, the memory block will be used again later, so
|
||||
/// it should not be decommitted. This value cannot be used with any other value. Using this value does not guarantee
|
||||
/// that the range operated on with MEM_RESET will contain zeros. If you want the range to contain zeros, decommit
|
||||
/// the memory and then recommit it. When you use MEM_RESET, the VirtualAllocEx function ignores the value of fProtect.
|
||||
/// However, you must still set fProtect to a valid protection value, such as PAGE_NOACCESS. VirtualAllocEx returns
|
||||
/// an error if you use MEM_RESET and the range of memory is mapped to a file. A shared view is only acceptable
|
||||
/// if it is mapped to a paging file.
|
||||
/// </summary>
|
||||
Reset = 0x80000,
|
||||
|
||||
/// <summary>
|
||||
/// MEM_RESET_UNDO should only be called on an address range to which MEM_RESET was successfully applied earlier.
|
||||
/// It indicates that the data in the specified memory range specified by lpAddress and dwSize is of interest to
|
||||
/// the caller and attempts to reverse the effects of MEM_RESET. If the function succeeds, that means all data in
|
||||
/// the specified address range is intact. If the function fails, at least some of the data in the address range
|
||||
/// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on
|
||||
/// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the
|
||||
/// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
|
||||
/// protection value, such as PAGE_NOACCESS.
|
||||
/// </summary>
|
||||
ResetUndo = 0x1000000,
|
||||
|
||||
/// <summary>
|
||||
/// Reserves an address range that can be used to map Address Windowing Extensions (AWE) pages. This value must
|
||||
/// be used with MEM_RESERVE and no other values.
|
||||
/// </summary>
|
||||
Physical = 0x400000,
|
||||
|
||||
/// <summary>
|
||||
/// Allocates memory at the highest possible address. This can be slower than regular allocations, especially when
|
||||
/// there are many allocations.
|
||||
/// </summary>
|
||||
TopDown = 0x100000,
|
||||
|
||||
/// <summary>
|
||||
/// Causes the system to track pages that are written to in the allocated region. If you specify this value, you
|
||||
/// must also specify MEM_RESERVE. To retrieve the addresses of the pages that have been written to since the region
|
||||
/// was allocated or the write-tracking state was reset, call the GetWriteWatch function. To reset the write-tracking
|
||||
/// state, call GetWriteWatch or ResetWriteWatch. The write-tracking feature remains enabled for the memory region
|
||||
/// until the region is freed.
|
||||
/// </summary>
|
||||
WriteWatch = 0x200000,
|
||||
|
||||
/// <summary>
|
||||
/// Allocates memory using large page support. The size and alignment must be a multiple of the large-page minimum.
|
||||
/// To obtain this value, use the GetLargePageMinimum function. If you specify this value, you must also specify
|
||||
/// MEM_RESERVE and MEM_COMMIT.
|
||||
/// </summary>
|
||||
LargePages = 0x20000000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unprefixed flags from CreateRemoteThread.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CreateThreadFlags
|
||||
{
|
||||
/// <summary>
|
||||
/// The thread runs immediately after creation.
|
||||
/// </summary>
|
||||
RunImmediately = 0x0,
|
||||
|
||||
/// <summary>
|
||||
/// The thread is created in a suspended state, and does not run until the ResumeThread function is called.
|
||||
/// </summary>
|
||||
CreateSuspended = 0x4,
|
||||
|
||||
/// <summary>
|
||||
/// The dwStackSize parameter specifies the initial reserve size of the stack. If this flag is not specified, dwStackSize specifies the commit size.
|
||||
/// </summary>
|
||||
StackSizeParamIsReservation = 0x10000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum DuplicateOptions : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// Closes the source handle. This occurs regardless of any error status returned.
|
||||
/// </summary>
|
||||
CloseSource = 0x00000001,
|
||||
|
||||
/// <summary>
|
||||
/// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle.
|
||||
/// </summary>
|
||||
SameAccess = 0x00000002,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PAGE_* from memoryapi.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MemoryProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables execute access to the committed region of pages. An attempt to write to the committed region results
|
||||
/// in an access violation. This flag is not supported by the CreateFileMapping function.
|
||||
/// </summary>
|
||||
Execute = 0x10,
|
||||
|
||||
/// <summary>
|
||||
/// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region
|
||||
/// results in an access violation.
|
||||
/// </summary>
|
||||
ExecuteRead = 0x20,
|
||||
|
||||
/// <summary>
|
||||
/// Enables execute, read-only, or read/write access to the committed region of pages.
|
||||
/// </summary>
|
||||
ExecuteReadWrite = 0x40,
|
||||
|
||||
/// <summary>
|
||||
/// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to
|
||||
/// write to a committed copy-on-write page results in a private copy of the page being made for the process. The
|
||||
/// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not
|
||||
/// supported by the VirtualAlloc or VirtualAllocEx functions.
|
||||
/// </summary>
|
||||
ExecuteWriteCopy = 0x80,
|
||||
|
||||
/// <summary>
|
||||
/// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed
|
||||
/// region results in an access violation. This flag is not supported by the CreateFileMapping function.
|
||||
/// </summary>
|
||||
NoAccess = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// Enables read-only access to the committed region of pages. An attempt to write to the committed region results
|
||||
/// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed
|
||||
/// region results in an access violation.
|
||||
/// </summary>
|
||||
ReadOnly = 0x02,
|
||||
|
||||
/// <summary>
|
||||
/// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled,
|
||||
/// attempting to execute code in the committed region results in an access violation.
|
||||
/// </summary>
|
||||
ReadWrite = 0x04,
|
||||
|
||||
/// <summary>
|
||||
/// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to
|
||||
/// a committed copy-on-write page results in a private copy of the page being made for the process. The private
|
||||
/// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is
|
||||
/// enabled, attempting to execute code in the committed region results in an access violation. This flag is not
|
||||
/// supported by the VirtualAlloc or VirtualAllocEx functions.
|
||||
/// </summary>
|
||||
WriteCopy = 0x08,
|
||||
|
||||
/// <summary>
|
||||
/// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like
|
||||
/// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations
|
||||
/// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable
|
||||
/// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect
|
||||
/// or CreateFileMapping functions.
|
||||
/// </summary>
|
||||
TargetsInvalid = 0x40000000,
|
||||
|
||||
/// <summary>
|
||||
/// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect.
|
||||
/// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information
|
||||
/// will be maintained while the page protection changes. This flag is only valid when the protection changes to
|
||||
/// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY.
|
||||
/// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
|
||||
/// targets for CFG.
|
||||
/// </summary>
|
||||
TargetsNoUpdate = TargetsInvalid,
|
||||
|
||||
/// <summary>
|
||||
/// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
|
||||
/// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time
|
||||
/// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn
|
||||
/// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a
|
||||
/// system service, the service typically returns a failure status indicator. This value cannot be used with
|
||||
/// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function.
|
||||
/// </summary>
|
||||
Guard = 0x100,
|
||||
|
||||
/// <summary>
|
||||
/// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required
|
||||
/// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an
|
||||
/// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS,
|
||||
/// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the
|
||||
/// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared
|
||||
/// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function.
|
||||
/// </summary>
|
||||
NoCache = 0x200,
|
||||
|
||||
/// <summary>
|
||||
/// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required
|
||||
/// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an
|
||||
/// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS,
|
||||
/// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory
|
||||
/// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access
|
||||
/// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function.
|
||||
/// </summary>
|
||||
WriteCombine = 0x400,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PROCESS_* from processthreadsapi.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ProcessAccessFlags : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// All possible access rights for a process object.
|
||||
/// </summary>
|
||||
AllAccess = 0x001F0FFF,
|
||||
|
||||
/// <summary>
|
||||
/// Required to create a process.
|
||||
/// </summary>
|
||||
CreateProcess = 0x0080,
|
||||
|
||||
/// <summary>
|
||||
/// Required to create a thread.
|
||||
/// </summary>
|
||||
CreateThread = 0x0002,
|
||||
|
||||
/// <summary>
|
||||
/// Required to duplicate a handle using DuplicateHandle.
|
||||
/// </summary>
|
||||
DupHandle = 0x0040,
|
||||
|
||||
/// <summary>
|
||||
/// Required to retrieve certain information about a process, such as its token, exit code,
|
||||
/// and priority class (see OpenProcessToken).
|
||||
/// </summary>
|
||||
QueryInformation = 0x0400,
|
||||
|
||||
/// <summary>
|
||||
/// Required to retrieve certain information about a process(see GetExitCodeProcess, GetPriorityClass, IsProcessInJob,
|
||||
/// QueryFullProcessImageName). A handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted
|
||||
/// PROCESS_QUERY_LIMITED_INFORMATION.
|
||||
/// </summary>
|
||||
QueryLimitedInformation = 0x1000,
|
||||
|
||||
/// <summary>
|
||||
/// Required to set certain information about a process, such as its priority class (see SetPriorityClass).
|
||||
/// </summary>
|
||||
SetInformation = 0x0200,
|
||||
|
||||
/// <summary>
|
||||
/// Required to set memory limits using SetProcessWorkingSetSize.
|
||||
/// </summary>
|
||||
SetQuote = 0x0100,
|
||||
|
||||
/// <summary>
|
||||
/// Required to suspend or resume a process.
|
||||
/// </summary>
|
||||
SuspendResume = 0x0800,
|
||||
|
||||
/// <summary>
|
||||
/// Required to terminate a process using TerminateProcess.
|
||||
/// </summary>
|
||||
Terminate = 0x0001,
|
||||
|
||||
/// <summary>
|
||||
/// Required to perform an operation on the address space of a process(see VirtualProtectEx and WriteProcessMemory).
|
||||
/// </summary>
|
||||
VmOperation = 0x0008,
|
||||
|
||||
/// <summary>
|
||||
/// Required to read memory in a process using ReadProcessMemory.
|
||||
/// </summary>
|
||||
VmRead = 0x0010,
|
||||
|
||||
/// <summary>
|
||||
/// Required to write to memory in a process using WriteProcessMemory.
|
||||
/// </summary>
|
||||
VmWrite = 0x0020,
|
||||
|
||||
/// <summary>
|
||||
/// Required to wait for the process to terminate using the wait functions.
|
||||
/// </summary>
|
||||
Synchronize = 0x00100000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WAIT_* from synchapi.
|
||||
/// </summary>
|
||||
public enum WaitResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The specified object is a mutex object that was not released by the thread that owned the mutex object
|
||||
/// before the owning thread terminated.Ownership of the mutex object is granted to the calling thread and
|
||||
/// the mutex state is set to nonsignaled. If the mutex was protecting persistent state information, you
|
||||
/// should check it for consistency.
|
||||
/// </summary>
|
||||
Abandoned = 0x80,
|
||||
|
||||
/// <summary>
|
||||
/// The state of the specified object is signaled.
|
||||
/// </summary>
|
||||
Object0 = 0x0,
|
||||
|
||||
/// <summary>
|
||||
/// The time-out interval elapsed, and the object's state is nonsignaled.
|
||||
/// </summary>
|
||||
Timeout = 0x102,
|
||||
|
||||
/// <summary>
|
||||
/// The function has failed. To get extended error information, call GetLastError.
|
||||
/// </summary>
|
||||
WAIT_FAILED = 0xFFFFFFF,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes an open object handle.
|
||||
/// </summary>
|
||||
/// <param name="hObject">
|
||||
/// A valid handle to an open object.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error
|
||||
/// information, call GetLastError. If the application is running under a debugger, the function will throw an exception if it receives
|
||||
/// either a handle value that is not valid or a pseudo-handle value. This can happen if you close a handle twice, or if you call
|
||||
/// CloseHandle on a handle returned by the FindFirstFile function instead of calling the FindClose function.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a thread that runs in the virtual address space of another process. Use the CreateRemoteThreadEx function
|
||||
/// to create a thread that runs in the virtual address space of another process and optionally specify extended attributes.
|
||||
/// </summary>
|
||||
/// <param name="hProcess">
|
||||
/// A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD,
|
||||
/// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without
|
||||
/// these rights on certain platforms. For more information, see Process Security and Access Rights.
|
||||
/// </param>
|
||||
/// <param name="lpThreadAttributes">
|
||||
/// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether
|
||||
/// child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor
|
||||
/// and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from
|
||||
/// the primary token of the creator.
|
||||
/// </param>
|
||||
/// <param name="dwStackSize">
|
||||
/// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is 0 (zero), the
|
||||
/// new thread uses the default size for the executable. For more information, see Thread Stack Size.
|
||||
/// </param>
|
||||
/// <param name="lpStartAddress">
|
||||
/// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the
|
||||
/// starting address of the thread in the remote process. The function must exist in the remote process. For more information,
|
||||
/// see ThreadProc.
|
||||
/// </param>
|
||||
/// <param name="lpParameter">
|
||||
/// A pointer to a variable to be passed to the thread function.
|
||||
/// </param>
|
||||
/// <param name="dwCreationFlags">
|
||||
/// The flags that control the creation of the thread.
|
||||
/// </param>
|
||||
/// <param name="lpThreadId">
|
||||
/// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is
|
||||
/// NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if lpStartAddress
|
||||
/// points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and
|
||||
/// the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process.
|
||||
/// This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to
|
||||
/// invalid or missing dynamic-link libraries (DLL).
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr CreateRemoteThread(
|
||||
IntPtr hProcess,
|
||||
IntPtr lpThreadAttributes,
|
||||
UIntPtr dwStackSize,
|
||||
nuint lpStartAddress,
|
||||
nuint lpParameter,
|
||||
CreateThreadFlags dwCreationFlags,
|
||||
out uint lpThreadId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the termination status of the specified thread.
|
||||
/// </summary>
|
||||
/// <param name="hThread">
|
||||
/// A handle to the thread. The handle must have the THREAD_QUERY_INFORMATION or THREAD_QUERY_LIMITED_INFORMATION
|
||||
/// access right.For more information, see Thread Security and Access Rights.
|
||||
/// </param>
|
||||
/// <param name="lpExitCode">
|
||||
/// A pointer to a variable to receive the thread termination status.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get
|
||||
/// extended error information, call GetLastError.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
|
||||
|
||||
/// <summary>
|
||||
/// Opens an existing local process object.
|
||||
/// </summary>
|
||||
/// <param name="dwDesiredAccess">
|
||||
/// The access to the process object. This access right is checked against the security descriptor for the process. This parameter can be one or
|
||||
/// more of the process access rights. If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the
|
||||
/// contents of the security descriptor.
|
||||
/// </param>
|
||||
/// <param name="bInheritHandle">
|
||||
/// If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle.
|
||||
/// </param>
|
||||
/// <param name="dwProcessId">
|
||||
/// The identifier of the local process to be opened. If the specified process is the System Idle Process(0x00000000), the function fails and the
|
||||
/// last error code is ERROR_INVALID_PARAMETER.If the specified process is the System process or one of the Client Server Run-Time Subsystem(CSRSS)
|
||||
/// processes, this function fails and the last error code is ERROR_ACCESS_DENIED because their access restrictions prevent user-level code from
|
||||
/// opening them. If you are using GetCurrentProcessId as an argument to this function, consider using GetCurrentProcess instead of OpenProcess, for
|
||||
/// improved performance.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is an open handle to the specified process.
|
||||
/// If the function fails, the return value is NULL.To get extended error information, call GetLastError.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr OpenProcess(
|
||||
ProcessAccessFlags dwDesiredAccess,
|
||||
bool bInheritHandle,
|
||||
int dwProcessId);
|
||||
|
||||
/// <summary>
|
||||
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex.
|
||||
/// Reserves, commits, or changes the state of a region of memory within the virtual address space of a specified process.
|
||||
/// The function initializes the memory it allocates to zero. To specify the NUMA node for the physical memory, see
|
||||
/// VirtualAllocExNuma.
|
||||
/// </summary>
|
||||
/// <param name="hProcess">
|
||||
/// The handle to a process. The function allocates memory within the virtual address space of this process. The handle
|
||||
/// must have the PROCESS_VM_OPERATION access right. For more information, see Process Security and Access Rights.
|
||||
/// </param>
|
||||
/// <param name="lpAddress">
|
||||
/// The pointer that specifies a desired starting address for the region of pages that you want to allocate. If you
|
||||
/// are reserving memory, the function rounds this address down to the nearest multiple of the allocation granularity.
|
||||
/// If you are committing memory that is already reserved, the function rounds this address down to the nearest page
|
||||
/// boundary. To determine the size of a page and the allocation granularity on the host computer, use the GetSystemInfo
|
||||
/// function. If lpAddress is NULL, the function determines where to allocate the region. If this address is within
|
||||
/// an enclave that you have not initialized by calling InitializeEnclave, VirtualAllocEx allocates a page of zeros
|
||||
/// for the enclave at that address. The page must be previously uncommitted, and will not be measured with the EEXTEND
|
||||
/// instruction of the Intel Software Guard Extensions programming model. If the address in within an enclave that you
|
||||
/// initialized, then the allocation operation fails with the ERROR_INVALID_ADDRESS error.
|
||||
/// </param>
|
||||
/// <param name="dwSize">
|
||||
/// The size of the region of memory to allocate, in bytes. If lpAddress is NULL, the function rounds dwSize up to the
|
||||
/// next page boundary. If lpAddress is not NULL, the function allocates all pages that contain one or more bytes in
|
||||
/// the range from lpAddress to lpAddress+dwSize. This means, for example, that a 2-byte range that straddles a page
|
||||
/// boundary causes the function to allocate both pages.
|
||||
/// </param>
|
||||
/// <param name="flAllocationType">
|
||||
/// The type of memory allocation. This parameter must contain one of the MEM_* enum values.
|
||||
/// </param>
|
||||
/// <param name="flProtect">
|
||||
/// The memory protection for the region of pages to be allocated. If the pages are being committed, you can specify
|
||||
/// any one of the memory protection constants.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is the base address of the allocated region of pages. If the function
|
||||
/// fails, the return value is NULL.To get extended error information, call GetLastError.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
|
||||
public static extern IntPtr VirtualAllocEx(
|
||||
IntPtr hProcess,
|
||||
IntPtr lpAddress,
|
||||
int dwSize,
|
||||
AllocationType flAllocationType,
|
||||
MemoryProtection flProtect);
|
||||
|
||||
/// <summary>
|
||||
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfreeex.
|
||||
/// Releases, decommits, or releases and decommits a region of memory within the virtual address space of a specified
|
||||
/// process.
|
||||
/// </summary>
|
||||
/// <param name="hProcess">
|
||||
/// A handle to a process. The function frees memory within the virtual address space of the process. The handle must
|
||||
/// have the PROCESS_VM_OPERATION access right.For more information, see Process Security and Access Rights.
|
||||
/// </param>
|
||||
/// <param name="lpAddress">
|
||||
/// A pointer to the starting address of the region of memory to be freed. If the dwFreeType parameter is MEM_RELEASE,
|
||||
/// lpAddress must be the base address returned by the VirtualAllocEx function when the region is reserved.
|
||||
/// </param>
|
||||
/// <param name="dwSize">
|
||||
/// The size of the region of memory to free, in bytes. If the dwFreeType parameter is MEM_RELEASE, dwSize must be 0
|
||||
/// (zero). The function frees the entire region that is reserved in the initial allocation call to VirtualAllocEx.
|
||||
/// If dwFreeType is MEM_DECOMMIT, the function decommits all memory pages that contain one or more bytes in the range
|
||||
/// from the lpAddress parameter to (lpAddress+dwSize). This means, for example, that a 2-byte region of memory that
|
||||
/// straddles a page boundary causes both pages to be decommitted. If lpAddress is the base address returned by
|
||||
/// VirtualAllocEx and dwSize is 0 (zero), the function decommits the entire region that is allocated by VirtualAllocEx.
|
||||
/// After that, the entire region is in the reserved state.
|
||||
/// </param>
|
||||
/// <param name="dwFreeType">
|
||||
/// The type of free operation. This parameter must be one of the MEM_* enum values.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is a nonzero value. If the function fails, the return value is 0 (zero).
|
||||
/// To get extended error information, call GetLastError.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
|
||||
public static extern bool VirtualFreeEx(
|
||||
IntPtr hProcess,
|
||||
IntPtr lpAddress,
|
||||
int dwSize,
|
||||
AllocationType dwFreeType);
|
||||
|
||||
/// <summary>
|
||||
/// Waits until the specified object is in the signaled state or the time-out interval elapses. To enter an alertable wait
|
||||
/// state, use the WaitForSingleObjectEx function.To wait for multiple objects, use WaitForMultipleObjects.
|
||||
/// </summary>
|
||||
/// <param name="hHandle">
|
||||
/// A handle to the object. For a list of the object types whose handles can be specified, see the following Remarks section.
|
||||
/// If this handle is closed while the wait is still pending, the function's behavior is undefined. The handle must have the
|
||||
/// SYNCHRONIZE access right. For more information, see Standard Access Rights.
|
||||
/// </param>
|
||||
/// <param name="dwMilliseconds">
|
||||
/// The time-out interval, in milliseconds. If a nonzero value is specified, the function waits until the object is signaled
|
||||
/// or the interval elapses. If dwMilliseconds is zero, the function does not enter a wait state if the object is not signaled;
|
||||
/// it always returns immediately. If dwMilliseconds is INFINITE, the function will return only when the object is signaled.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value indicates the event that caused the function to return.
|
||||
/// It can be one of the WaitResult values.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or
|
||||
/// the operation fails.
|
||||
/// </summary>
|
||||
/// <param name="hProcess">
|
||||
/// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and PROCESS_VM_OPERATION access
|
||||
/// to the process.
|
||||
/// </param>
|
||||
/// <param name="lpBaseAddress">
|
||||
/// A pointer to the base address in the specified process to which data is written. Before data transfer occurs, the
|
||||
/// system verifies that all data in the base address and memory of the specified size is accessible for write access,
|
||||
/// and if it is not accessible, the function fails.
|
||||
/// </param>
|
||||
/// <param name="lpBuffer">
|
||||
/// A pointer to the buffer that contains data to be written in the address space of the specified process.
|
||||
/// </param>
|
||||
/// <param name="dwSize">
|
||||
/// The number of bytes to be written to the specified process.
|
||||
/// </param>
|
||||
/// <param name="lpNumberOfBytesWritten">
|
||||
/// A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter
|
||||
/// is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
|
||||
/// extended error information, call GetLastError.The function fails if the requested write operation crosses into an
|
||||
/// area of the process that is inaccessible.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool WriteProcessMemory(
|
||||
IntPtr hProcess,
|
||||
IntPtr lpBaseAddress,
|
||||
byte[] lpBuffer,
|
||||
int dwSize,
|
||||
out IntPtr lpNumberOfBytesWritten);
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates an object handle.
|
||||
/// </summary>
|
||||
/// <param name="hSourceProcessHandle">
|
||||
/// A handle to the process with the handle to be duplicated.
|
||||
///
|
||||
/// The handle must have the PROCESS_DUP_HANDLE access right.
|
||||
/// </param>
|
||||
/// <param name="hSourceHandle">
|
||||
/// The handle to be duplicated. This is an open object handle that is valid in the context of the source process.
|
||||
/// For a list of objects whose handles can be duplicated, see the following Remarks section.
|
||||
/// </param>
|
||||
/// <param name="hTargetProcessHandle">
|
||||
/// A handle to the process that is to receive the duplicated handle.
|
||||
///
|
||||
/// The handle must have the PROCESS_DUP_HANDLE access right.
|
||||
/// </param>
|
||||
/// <param name="lpTargetHandle">
|
||||
/// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process.
|
||||
///
|
||||
/// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively.
|
||||
///
|
||||
/// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates.
|
||||
///
|
||||
/// This parameter is ignored if hTargetProcessHandle is NULL.
|
||||
/// </param>
|
||||
/// <param name="dwDesiredAccess">
|
||||
/// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section.
|
||||
///
|
||||
/// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated.
|
||||
///
|
||||
/// This parameter is ignored if hTargetProcessHandle is NULL.
|
||||
/// </param>
|
||||
/// <param name="bInheritHandle">
|
||||
/// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited.
|
||||
///
|
||||
/// This parameter is ignored if hTargetProcessHandle is NULL.
|
||||
/// </param>
|
||||
/// <param name="dwOptions">
|
||||
/// Optional actions.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is nonzero.
|
||||
///
|
||||
/// If the function fails, the return value is zero. To get extended error information, call GetLastError.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle.
|
||||
/// </remarks>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool DuplicateHandle(
|
||||
IntPtr hSourceProcessHandle,
|
||||
IntPtr hSourceHandle,
|
||||
IntPtr hTargetProcessHandle,
|
||||
out IntPtr lpTargetHandle,
|
||||
uint dwDesiredAccess,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
|
||||
DuplicateOptions dwOptions);
|
||||
|
||||
/// <summary>
|
||||
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew.
|
||||
/// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To
|
||||
/// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
|
||||
/// </summary>
|
||||
/// <param name="lpModuleName">
|
||||
/// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default
|
||||
/// library extension .dll is appended. The file name string can include a trailing point character (.) to indicate
|
||||
/// that the module name has no extension. The string does not have to specify a path. When specifying a path, be sure
|
||||
/// to use backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules
|
||||
/// currently mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns
|
||||
/// a handle to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve
|
||||
/// handles for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
|
||||
/// value is NULL.To get extended error information, call GetLastError.
|
||||
/// </returns>
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
|
||||
public static extern IntPtr GetModuleHandleW(string lpModuleName);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
|
||||
/// </summary>
|
||||
/// <param name="hModule">
|
||||
/// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
|
||||
/// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
|
||||
/// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
|
||||
/// </param>
|
||||
/// <param name="procName">
|
||||
/// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
|
||||
/// in the low-order word; the high-order word must be zero.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is the address of the exported function or variable. If the function
|
||||
/// fails, the return value is NULL.To get extended error information, call GetLastError.
|
||||
/// </returns>
|
||||
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
|
||||
[SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")]
|
||||
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
|
||||
}
|
||||
}
|
||||
4
Dalamud.Injector/NativeMethods.json
Normal file
4
Dalamud.Injector/NativeMethods.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"allowMarshaling": false
|
||||
}
|
||||
8
Dalamud.Injector/NativeMethods.txt
Normal file
8
Dalamud.Injector/NativeMethods.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
CreateRemoteThread
|
||||
WaitForSingleObject
|
||||
GetExitCodeThread
|
||||
DuplicateHandle
|
||||
|
||||
MessageBox
|
||||
GetModuleHandle
|
||||
GetProcAddress
|
||||
|
|
@ -12,48 +12,33 @@ using System.Text.RegularExpressions;
|
|||
using Dalamud.Common;
|
||||
using Dalamud.Common.Game;
|
||||
using Dalamud.Common.Util;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Reloaded.Memory.Buffers;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
using static Dalamud.Injector.NativeFunctions;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Dalamud.Injector
|
||||
{
|
||||
/// <summary>
|
||||
/// Entrypoint to the program.
|
||||
/// </summary>
|
||||
public sealed class EntryPoint
|
||||
public sealed class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
|
||||
/// </summary>
|
||||
/// <param name="argc">Count of arguments.</param>
|
||||
/// <param name="argvPtr">char** string arguments.</param>
|
||||
/// <returns>Return value (HRESULT).</returns>
|
||||
public delegate int MainDelegate(int argc, IntPtr argvPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Start the Dalamud injector.
|
||||
/// </summary>
|
||||
/// <param name="argc">Count of arguments.</param>
|
||||
/// <param name="argvPtr">byte** string arguments.</param>
|
||||
/// <param name="argsArray">Command line arguments.</param>
|
||||
/// <returns>Return value (HRESULT).</returns>
|
||||
public static int Main(int argc, IntPtr argvPtr)
|
||||
public static int Main(string[] argsArray)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string> args = new(argc);
|
||||
|
||||
unsafe
|
||||
{
|
||||
var argv = (IntPtr*)argvPtr;
|
||||
for (var i = 0; i < argc; i++)
|
||||
args.Add(Marshal.PtrToStringUni(argv[i]));
|
||||
}
|
||||
// API14 TODO: Refactor
|
||||
var args = argsArray.ToList();
|
||||
args.Insert(0, Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
Init(args);
|
||||
args.Remove("-v"); // Remove "verbose" flag
|
||||
|
|
@ -268,9 +253,9 @@ namespace Dalamud.Injector
|
|||
|
||||
private static OSPlatform DetectPlatformHeuristic()
|
||||
{
|
||||
var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll");
|
||||
var wineServerCallPtr = NativeFunctions.GetProcAddress(ntdll, "wine_server_call");
|
||||
var wineGetHostVersionPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version");
|
||||
var ntdll = Windows.Win32.PInvoke.GetModuleHandle("ntdll.dll");
|
||||
var wineServerCallPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_server_call");
|
||||
var wineGetHostVersionPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_get_host_version");
|
||||
var winePlatform = GetWinePlatform(wineGetHostVersionPtr);
|
||||
var isWine = wineServerCallPtr != nint.Zero;
|
||||
|
||||
|
|
@ -306,6 +291,7 @@ namespace Dalamud.Injector
|
|||
var configurationPath = startInfo.ConfigurationPath;
|
||||
var pluginDirectory = startInfo.PluginDirectory;
|
||||
var assetDirectory = startInfo.AssetDirectory;
|
||||
var tempDirectory = startInfo.TempDirectory;
|
||||
var delayInitializeMs = startInfo.DelayInitializeMs;
|
||||
var logName = startInfo.LogName;
|
||||
var logPath = startInfo.LogPath;
|
||||
|
|
@ -336,6 +322,10 @@ namespace Dalamud.Injector
|
|||
{
|
||||
assetDirectory = args[i][key.Length..];
|
||||
}
|
||||
else if (args[i].StartsWith(key = "--dalamud-temp-directory="))
|
||||
{
|
||||
tempDirectory = args[i][key.Length..];
|
||||
}
|
||||
else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
|
||||
{
|
||||
delayInitializeMs = int.Parse(args[i][key.Length..]);
|
||||
|
|
@ -448,6 +438,7 @@ namespace Dalamud.Injector
|
|||
startInfo.ConfigurationPath = configurationPath;
|
||||
startInfo.PluginDirectory = pluginDirectory;
|
||||
startInfo.AssetDirectory = assetDirectory;
|
||||
startInfo.TempDirectory = tempDirectory;
|
||||
startInfo.Language = clientLanguage;
|
||||
startInfo.Platform = platform;
|
||||
startInfo.DelayInitializeMs = delayInitializeMs;
|
||||
|
|
@ -621,10 +612,13 @@ namespace Dalamud.Injector
|
|||
|
||||
if (warnManualInjection)
|
||||
{
|
||||
var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel);
|
||||
var result = Windows.Win32.PInvoke.MessageBox(
|
||||
HWND.Null,
|
||||
$"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.",
|
||||
"Dalamud",
|
||||
MESSAGEBOX_STYLE.MB_ICONWARNING | MESSAGEBOX_STYLE.MB_OKCANCEL);
|
||||
|
||||
// IDCANCEL
|
||||
if (result == 2)
|
||||
if (result == MESSAGEBOX_RESULT.IDCANCEL)
|
||||
{
|
||||
Log.Information("User cancelled injection");
|
||||
return -2;
|
||||
|
|
@ -934,30 +928,48 @@ namespace Dalamud.Injector
|
|||
Inject(process, startInfo, false);
|
||||
}
|
||||
|
||||
var processHandleForOwner = IntPtr.Zero;
|
||||
var processHandleForOwner = HANDLE.Null;
|
||||
if (handleOwner != IntPtr.Zero)
|
||||
{
|
||||
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess))
|
||||
unsafe
|
||||
{
|
||||
Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
|
||||
if (!Windows.Win32.PInvoke.DuplicateHandle(
|
||||
new HANDLE(Process.GetCurrentProcess().Handle.ToPointer()),
|
||||
new HANDLE(process.Handle.ToPointer()),
|
||||
new HANDLE(handleOwner),
|
||||
&processHandleForOwner,
|
||||
0,
|
||||
false,
|
||||
DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS))
|
||||
{
|
||||
Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}");
|
||||
Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {(IntPtr)processHandleForOwner}}}");
|
||||
|
||||
Log.CloseAndFlush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Process GetInheritableCurrentProcessHandle()
|
||||
private static unsafe Process GetInheritableCurrentProcessHandle()
|
||||
{
|
||||
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess))
|
||||
var currentProcessHandle = new HANDLE(Process.GetCurrentProcess().Handle.ToPointer());
|
||||
var inheritableHandle = HANDLE.Null;
|
||||
if (!Windows.Win32.PInvoke.DuplicateHandle(
|
||||
currentProcessHandle,
|
||||
currentProcessHandle,
|
||||
currentProcessHandle,
|
||||
&inheritableHandle,
|
||||
0,
|
||||
true,
|
||||
DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS))
|
||||
{
|
||||
Log.Error("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
|
||||
return null;
|
||||
throw new Win32Exception("Failed to call DuplicateHandle");
|
||||
}
|
||||
|
||||
return new ExistingProcess(inheritableCurrentProcessHandle);
|
||||
return new ExistingProcess(inheritableHandle);
|
||||
}
|
||||
|
||||
private static int ProcessLaunchTestCommand(List<string> args)
|
||||
|
|
@ -1048,13 +1060,13 @@ namespace Dalamud.Injector
|
|||
}
|
||||
|
||||
injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress);
|
||||
injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode);
|
||||
var exitCode = injector.CallRemoteFunction(initAddress, startInfoAddress);
|
||||
|
||||
// ======================================================
|
||||
|
||||
if (exitCode > 0)
|
||||
{
|
||||
Log.Error($"Dalamud.Boot::Initialize returned {exitCode}");
|
||||
Log.Error("Dalamud.Boot::Initialize returned {ExitCode}", exitCode);
|
||||
return;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
|
@ -42,19 +42,19 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageReference Include="xunit.analyzers" Version="0.10.0" />
|
||||
<PackageReference Include="xunit.assert" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.core" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.extensibility.execution" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.console" Version="2.4.1">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.abstractions" />
|
||||
<PackageReference Include="xunit.analyzers" />
|
||||
<PackageReference Include="xunit.assert" />
|
||||
<PackageReference Include="xunit.core" />
|
||||
<PackageReference Include="xunit.extensibility.core" />
|
||||
<PackageReference Include="xunit.extensibility.execution" />
|
||||
<PackageReference Include="xunit.runner.console">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Game.Text.SeStringHandling
|
||||
|
|
@ -50,19 +54,41 @@ namespace Dalamud.Test.Game.Text.SeStringHandling
|
|||
var config = new MockConfig { Text = seString };
|
||||
PluginConfigurations.SerializeConfig(config);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void TestConfigDeserializable()
|
||||
{
|
||||
var builder = new SeStringBuilder();
|
||||
var seString = builder.AddText("Some text").Build();
|
||||
var config = new MockConfig { Text = seString };
|
||||
|
||||
|
||||
// This relies on the type information being maintained, which is why we're using these
|
||||
// static methods instead of default serialization/deserialization.
|
||||
var configSerialized = PluginConfigurations.SerializeConfig(config);
|
||||
var configDeserialized = (MockConfig)PluginConfigurations.DeserializeConfig(configSerialized);
|
||||
Assert.Equal(config, configDeserialized);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(49, 209)]
|
||||
[InlineData(71, 7)]
|
||||
[InlineData(62, 116)]
|
||||
public void TestAutoTranslatePayloadReencode(uint group, uint key)
|
||||
{
|
||||
var payload = new AutoTranslatePayload(group, key);
|
||||
|
||||
Assert.Equal(group, payload.Group);
|
||||
Assert.Equal(key, payload.Key);
|
||||
|
||||
var encoded = payload.Encode();
|
||||
using var stream = new MemoryStream(encoded);
|
||||
using var reader = new BinaryReader(stream);
|
||||
var decodedPayload = Payload.Decode(reader) as AutoTranslatePayload;
|
||||
|
||||
Assert.Equal(group, decodedPayload.Group);
|
||||
Assert.Equal(key, decodedPayload.Key);
|
||||
|
||||
Assert.Equal(encoded, decodedPayload.Encode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,19 +31,19 @@ public class ReliableFileStorageTests
|
|||
.Select(
|
||||
i => Parallel.ForEachAsync(
|
||||
Enumerable.Range(1, 100),
|
||||
(j, _) =>
|
||||
async (j, _) =>
|
||||
{
|
||||
if (i % 2 == 0)
|
||||
{
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
rfs.Instance.WriteAllText(tempFile, j.ToString());
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, j.ToString());
|
||||
}
|
||||
else if (i % 3 == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
rfs.Instance.ReadAllText(tempFile);
|
||||
await rfs.Instance.ReadAllTextAsync(tempFile);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
|
|
@ -54,8 +54,6 @@ public class ReliableFileStorageTests
|
|||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
})));
|
||||
}
|
||||
|
||||
|
|
@ -112,41 +110,41 @@ public class ReliableFileStorageTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_WhenFileInBackup_ReturnsTrue()
|
||||
public async Task Exists_WhenFileInBackup_ReturnsTrue()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
|
||||
File.Delete(tempFile);
|
||||
Assert.True(rfs.Instance.Exists(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
|
||||
public async Task Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
|
||||
File.Delete(tempFile);
|
||||
Assert.False(rfs.Instance.Exists(tempFile, Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_ThrowsIfPathIsEmpty()
|
||||
public async Task WriteAllText_ThrowsIfPathIsEmpty()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentException>(() => rfs.Instance.WriteAllText("", TestFileContent1));
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () => await rfs.Instance.WriteAllTextAsync("", TestFileContent1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_ThrowsIfPathIsNull()
|
||||
public async Task WriteAllText_ThrowsIfPathIsNull()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentNullException>(() => rfs.Instance.WriteAllText(null!, TestFileContent1));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await rfs.Instance.WriteAllTextAsync(null!, TestFileContent1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -155,26 +153,26 @@ public class ReliableFileStorageTests
|
|||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_SeparatesContainers()
|
||||
public async Task WriteAllText_SeparatesContainers()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
var containerId = Guid.NewGuid();
|
||||
|
||||
using var rfs = CreateRfs();
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent2, containerId);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2, containerId);
|
||||
File.Delete(tempFile);
|
||||
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true, containerId));
|
||||
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true, containerId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -183,7 +181,7 @@ public class ReliableFileStorageTests
|
|||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateFailedRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
|
||||
|
|
@ -195,38 +193,38 @@ public class ReliableFileStorageTests
|
|||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent2);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent2, await File.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_SupportsNullContent()
|
||||
public async Task WriteAllText_SupportsNullContent()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, null);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, null);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal("", rfs.Instance.ReadAllText(tempFile));
|
||||
Assert.Equal("", await rfs.Instance.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_ThrowsIfPathIsEmpty()
|
||||
public async Task ReadAllText_ThrowsIfPathIsEmpty()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentException>(() => rfs.Instance.ReadAllText(""));
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () => await rfs.Instance.ReadAllTextAsync(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_ThrowsIfPathIsNull()
|
||||
public async Task ReadAllText_ThrowsIfPathIsNull()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentNullException>(() => rfs.Instance.ReadAllText(null!));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await rfs.Instance.ReadAllTextAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -236,40 +234,40 @@ public class ReliableFileStorageTests
|
|||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
|
||||
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
|
||||
public async Task ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
File.Delete(tempFile);
|
||||
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
|
||||
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
|
||||
public async Task ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
var containerId = Guid.NewGuid();
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
File.Delete(tempFile);
|
||||
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, containerId: containerId));
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, containerId: containerId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
|
||||
public async Task ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateFailedRfs();
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile));
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -278,7 +276,7 @@ public class ReliableFileStorageTests
|
|||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
using var rfs = CreateRfs();
|
||||
rfs.Instance.ReadAllText(tempFile, text => Assert.Equal(TestFileContent1, text));
|
||||
await rfs.Instance.ReadAllTextAsync(tempFile, text => Assert.Equal(TestFileContent1, text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -290,7 +288,7 @@ public class ReliableFileStorageTests
|
|||
var readerCalledOnce = false;
|
||||
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<FileReadException>(() => rfs.Instance.ReadAllText(tempFile, Reader));
|
||||
await Assert.ThrowsAsync<FileReadException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, Reader));
|
||||
|
||||
return;
|
||||
|
||||
|
|
@ -303,7 +301,7 @@ public class ReliableFileStorageTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
|
||||
public async Task ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
|
||||
|
|
@ -311,10 +309,10 @@ public class ReliableFileStorageTests
|
|||
var assertionCalled = false;
|
||||
|
||||
using var rfs = CreateRfs();
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
File.Delete(tempFile);
|
||||
|
||||
rfs.Instance.ReadAllText(tempFile, Reader);
|
||||
await rfs.Instance.ReadAllTextAsync(tempFile, Reader);
|
||||
Assert.True(assertionCalled);
|
||||
|
||||
return;
|
||||
|
|
@ -335,17 +333,17 @@ public class ReliableFileStorageTests
|
|||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, _ => throw new FileNotFoundException()));
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, _ => throw new FileNotFoundException()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
|
||||
public async Task ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, forceBackup));
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup));
|
||||
}
|
||||
|
||||
private static DisposableReliableFileStorage CreateRfs()
|
||||
|
|
|
|||
41
Dalamud.sln
41
Dalamud.sln
|
|
@ -1,4 +1,4 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32319.34
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
|
|
@ -7,10 +7,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
tools\BannedSymbols.txt = tools\BannedSymbols.txt
|
||||
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
|
||||
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
|
||||
tools\dalamud.ruleset = tools\dalamud.ruleset
|
||||
Directory.Build.props = Directory.Build.props
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
|
||||
|
|
@ -26,8 +25,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
|
||||
|
|
@ -48,8 +45,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
|
||||
|
|
@ -80,6 +75,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
generators\Directory.Build.props = generators\Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Sample", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Sample\Dalamud.EnumGenerator.Sample.csproj", "{8CDAEB2D-5022-450A-A97F-181C6270185F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Tests", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Tests\Dalamud.EnumGenerator.Tests.csproj", "{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -102,10 +108,6 @@ Global
|
|||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
|
|
@ -182,13 +184,23 @@ Global
|
|||
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
|
||||
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
|
||||
|
|
@ -208,6 +220,9 @@ Global
|
|||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
|
||||
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
|
||||
{8CDAEB2D-5022-450A-A97F-181C6270185F} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
|
||||
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ using System.Threading.Tasks;
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||
using Dalamud.Interface.Style;
|
||||
|
|
@ -20,9 +19,12 @@ using Dalamud.Plugin.Internal.AutoUpdate;
|
|||
using Dalamud.Plugin.Internal.Profiles;
|
||||
using Dalamud.Storage;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Dalamud.Configuration.Internal;
|
||||
|
|
@ -91,7 +93,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// <summary>
|
||||
/// Gets or sets a dictionary of seen FTUE levels.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> SeenFtueLevels { get; set; } = new();
|
||||
public Dictionary<string, int> SeenFtueLevels { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last loaded Dalamud version.
|
||||
|
|
@ -108,15 +110,10 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
public bool DoPluginTest { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key to opt into Dalamud staging builds.
|
||||
/// </summary>
|
||||
public string? DalamudBetaKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of custom repos.
|
||||
/// </summary>
|
||||
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
|
||||
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed.
|
||||
|
|
@ -126,12 +123,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// <summary>
|
||||
/// Gets or sets a list of hidden plugins.
|
||||
/// </summary>
|
||||
public List<string> HiddenPluginInternalName { get; set; } = new();
|
||||
public List<string> HiddenPluginInternalName { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of seen plugins.
|
||||
/// </summary>
|
||||
public List<string> SeenPluginInternalName { get; set; } = new();
|
||||
public List<string> SeenPluginInternalName { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of additional settings for devPlugins. The key is the absolute path
|
||||
|
|
@ -139,14 +136,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// However by specifiying this value manually, you can add arbitrary files outside the normal
|
||||
/// file paths.
|
||||
/// </summary>
|
||||
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = new();
|
||||
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of additional locations that dev plugins should be loaded from. This can
|
||||
/// be either a DLL or folder, but should be the absolute path, or a path relative to the currently
|
||||
/// injected Dalamud instance.
|
||||
/// </summary>
|
||||
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = new();
|
||||
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the global UI scale.
|
||||
|
|
@ -228,7 +225,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// <summary>
|
||||
/// Gets or sets a list representing the command history for the Dalamud Console.
|
||||
/// </summary>
|
||||
public List<string> LogCommandHistory { get; set; } = new();
|
||||
public List<string> LogCommandHistory { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the dev bar should open at startup.
|
||||
|
|
@ -250,7 +247,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// This setting is effected by the in-game "System Sounds" option and volume.
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
|
||||
public bool EnablePluginUISoundEffects { get; set; }
|
||||
public bool EnablePluginUISoundEffects { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether an additional button allowing pinning and clickthrough options should be shown
|
||||
|
|
@ -278,11 +275,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
|
||||
/// </summary>
|
||||
public string? DalamudBetaKind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether any plugin should be loaded when the game is started.
|
||||
/// It is reset immediately when read.
|
||||
|
|
@ -497,19 +489,37 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f);
|
||||
|
||||
#pragma warning disable SA1600
|
||||
#pragma warning disable SA1516
|
||||
// XLCore/XoM compatibility until they move it out
|
||||
public string? DalamudBetaKey { get; set; } = null;
|
||||
public string? DalamudBetaKind { get; set; }
|
||||
#pragma warning restore SA1516
|
||||
#pragma warning restore SA1600
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of badge passwords used to unlock badges.
|
||||
/// </summary>
|
||||
public List<string> UsedBadgePasswords { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether badges should be shown on the title screen.
|
||||
/// </summary>
|
||||
public bool ShowBadgesOnTitleScreen { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Load a configuration from the provided path.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to read from.</param>
|
||||
/// <param name="fs">File storage.</param>
|
||||
/// <returns>The deserialized configuration file.</returns>
|
||||
public static DalamudConfiguration Load(string path, ReliableFileStorage fs)
|
||||
public static async Task<DalamudConfiguration> Load(string path, ReliableFileStorage fs)
|
||||
{
|
||||
DalamudConfiguration deserialized = null;
|
||||
|
||||
try
|
||||
{
|
||||
fs.ReadAllText(path, text =>
|
||||
await fs.ReadAllTextAsync(path, text =>
|
||||
{
|
||||
deserialized =
|
||||
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
|
||||
|
|
@ -580,8 +590,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
{
|
||||
this.Save();
|
||||
this.isSaveQueued = false;
|
||||
|
||||
Log.Verbose("Config saved");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -593,7 +601,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
{
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium
|
||||
var winAnimEnabled = 0;
|
||||
var success = false;
|
||||
bool success;
|
||||
unsafe
|
||||
{
|
||||
success = Windows.Win32.PInvoke.SystemParametersInfo(
|
||||
|
|
@ -630,16 +638,20 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
// Wait for previous write to finish
|
||||
this.writeTask?.Wait();
|
||||
|
||||
this.writeTask = Task.Run(() =>
|
||||
this.writeTask = Task.Run(async () =>
|
||||
{
|
||||
Service<ReliableFileStorage>.Get().WriteAllText(
|
||||
this.configPath,
|
||||
JsonConvert.SerializeObject(this, SerializerSettings));
|
||||
await Service<ReliableFileStorage>.Get().WriteAllTextAsync(
|
||||
this.configPath,
|
||||
JsonConvert.SerializeObject(this, SerializerSettings));
|
||||
Log.Verbose("DalamudConfiguration saved");
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Log.Error(t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath);
|
||||
Log.Error(
|
||||
t.Exception,
|
||||
"Failed to save DalamudConfiguration to {Path}",
|
||||
this.configPath);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -31,5 +31,5 @@ internal sealed class DevPluginSettings
|
|||
/// <summary>
|
||||
/// Gets or sets a list of validation problems that have been dismissed by the user.
|
||||
/// </summary>
|
||||
public List<string> DismissedValidationProblems { get; set; } = new();
|
||||
public List<string> DismissedValidationProblems { get; set; } = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ using System.IO;
|
|||
using System.Reflection;
|
||||
|
||||
using Dalamud.Storage;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Configuration;
|
||||
|
|
@ -9,6 +11,7 @@ namespace Dalamud.Configuration;
|
|||
/// <summary>
|
||||
/// Configuration to store settings for a dalamud plugin.
|
||||
/// </summary>
|
||||
[Api15ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
|
||||
public sealed class PluginConfigurations
|
||||
{
|
||||
private readonly DirectoryInfo configDirectory;
|
||||
|
|
@ -36,7 +39,7 @@ public sealed class PluginConfigurations
|
|||
public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId)
|
||||
{
|
||||
Service<ReliableFileStorage>.Get()
|
||||
.WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
|
||||
.WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -52,12 +55,12 @@ public sealed class PluginConfigurations
|
|||
IPluginConfiguration? config = null;
|
||||
try
|
||||
{
|
||||
Service<ReliableFileStorage>.Get().ReadAllText(path.FullName, text =>
|
||||
Service<ReliableFileStorage>.Get().ReadAllTextAsync(path.FullName, text =>
|
||||
{
|
||||
config = DeserializeConfig(text);
|
||||
if (config == null)
|
||||
throw new Exception("Read config was null.");
|
||||
}, workingPluginId);
|
||||
}, workingPluginId).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
|
@ -17,9 +17,9 @@ namespace Dalamud.Console;
|
|||
[ServiceManager.BlockingEarlyLoadedService("Console is needed by other blocking early loaded services.")]
|
||||
internal partial class ConsoleManager : IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("CON");
|
||||
private static readonly ModuleLog Log = ModuleLog.Create<ConsoleManager>();
|
||||
|
||||
private Dictionary<string, IConsoleEntry> entries = new();
|
||||
private Dictionary<string, IConsoleEntry> entries = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConsoleManager"/> class.
|
||||
|
|
@ -99,10 +99,7 @@ internal partial class ConsoleManager : IServiceType
|
|||
ArgumentNullException.ThrowIfNull(name);
|
||||
ArgumentNullException.ThrowIfNull(alias);
|
||||
|
||||
var target = this.FindEntry(name);
|
||||
if (target == null)
|
||||
throw new EntryNotFoundException(name);
|
||||
|
||||
var target = this.FindEntry(name) ?? throw new EntryNotFoundException(name);
|
||||
if (this.FindEntry(alias) != null)
|
||||
throw new InvalidOperationException($"Entry '{alias}' already exists.");
|
||||
|
||||
|
|
@ -346,7 +343,7 @@ internal partial class ConsoleManager : IServiceType
|
|||
|
||||
private static class Traits
|
||||
{
|
||||
public static void ThrowIfTIsNullableAndNull<T>(T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
|
||||
public static void ThrowIfTIsNullableAndNull<T>(T? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
|
||||
{
|
||||
if (argument == null && !typeof(T).IsValueType)
|
||||
throw new ArgumentNullException(paramName);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly ConsoleManager console = Service<ConsoleManager>.Get();
|
||||
|
||||
private readonly List<IConsoleEntry> trackedEntries = new();
|
||||
private readonly List<IConsoleEntry> trackedEntries = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConsoleManagerPluginScoped"/> class.
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ using System.Threading.Tasks;
|
|||
using Dalamud.Common;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Hooking.Internal.Verification;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Storage;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Security;
|
||||
|
||||
|
|
@ -73,6 +76,11 @@ internal sealed unsafe class Dalamud : IServiceType
|
|||
scanner,
|
||||
Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
|
||||
|
||||
using (Timings.Start("HookVerifier Init"))
|
||||
{
|
||||
HookVerifier.Initialize(scanner);
|
||||
}
|
||||
|
||||
// Set up FFXIVClientStructs
|
||||
this.SetupClientStructsResolver(cacheDir);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<PropertyGroup Label="Feature">
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<DalamudVersion>13.0.0.1</DalamudVersion>
|
||||
<DalamudVersion>14.0.2.2</DalamudVersion>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
<FileVersion>$(DalamudVersion)</FileVersion>
|
||||
|
|
@ -61,37 +61,41 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BitFaster.Caching" Version="2.4.1" />
|
||||
<PackageReference Include="CheapLoc" Version="1.1.8" />
|
||||
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4" PrivateAssets="all" />
|
||||
<PackageReference Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp5" />
|
||||
<PackageReference Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp3" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
|
||||
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
|
||||
<PackageReference Include="BitFaster.Caching" />
|
||||
<PackageReference Include="CheapLoc" />
|
||||
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
|
||||
<PackageReference Include="goatcorp.Reloaded.Hooks" />
|
||||
<PackageReference Include="JetBrains.Annotations" />
|
||||
<PackageReference Include="Lumina" />
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MinSharp" Version="1.0.4" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
|
||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PackageReference Include="MinSharp" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="sqlite-net-pcl" />
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.0" />
|
||||
<PackageReference Include="System.Resources.Extensions" Version="8.0.0" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
|
||||
<PackageReference Include="System.Reactive" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="EnumCloneMap.txt"/>
|
||||
<AdditionalFiles Include="EnumCloneMap.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
|
||||
<LogicalName>imgui-frag.hlsl.bytes</LogicalName>
|
||||
|
|
@ -100,6 +104,7 @@
|
|||
<LogicalName>imgui-vertex.hlsl.bytes</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
|
||||
<ProjectReference Include="..\imgui\Dalamud.Bindings.ImGuizmo\Dalamud.Bindings.ImGuizmo.csproj" />
|
||||
|
|
@ -120,6 +125,8 @@
|
|||
<Content Include="licenses.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="Interface\ImGuiBackend\Renderers\gaussian.hlsl" />
|
||||
<None Remove="Interface\ImGuiBackend\Renderers\fullscreen-quad.hlsl.bytes" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -160,6 +167,9 @@
|
|||
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" describe --tags --always --dirty" ConsoleToMSBuild="true">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitDescribeOutput" />
|
||||
</Exec>
|
||||
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" rev-parse --abbrev-ref HEAD" ConsoleToMSBuild="true">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitBranch" />
|
||||
</Exec>
|
||||
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))\..\lib\FFXIVClientStructs" describe --long --always --dirty" ConsoleToMSBuild="true">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="ClientStructsGitDescribeOutput" />
|
||||
</Exec>
|
||||
|
|
@ -167,6 +177,7 @@
|
|||
<PropertyGroup>
|
||||
<CommitCount>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", ""))</CommitCount>
|
||||
<CommitHash>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitHash), @"\t|\n|\r", ""))</CommitHash>
|
||||
<Branch>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitBranch), @"\t|\n|\r", ""))</Branch>
|
||||
<SCMVersion>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", ""))</SCMVersion>
|
||||
<CommitHashClientStructs>$([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", ""))</CommitHashClientStructs>
|
||||
</PropertyGroup>
|
||||
|
|
@ -180,6 +191,7 @@
|
|||
<!-- stub out version since it takes a while. -->
|
||||
<PropertyGroup>
|
||||
<SCMVersion>Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss))</SCMVersion>
|
||||
<Branch>???</Branch>
|
||||
<CommitHashClientStructs>???</CommitHashClientStructs>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
|
|
@ -203,6 +215,10 @@
|
|||
<_Parameter1>GitCommitCount</_Parameter1>
|
||||
<_Parameter2>$(CommitCount)</_Parameter2>
|
||||
</AssemblyAttributes>
|
||||
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(Branch)' != ''">
|
||||
<_Parameter1>GitBranch</_Parameter1>
|
||||
<_Parameter2>$(Branch)</_Parameter2>
|
||||
</AssemblyAttributes>
|
||||
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(CommitHashClientStructs)' != ''">
|
||||
<_Parameter1>GitHashClientStructs</_Parameter1>
|
||||
<_Parameter2>$(CommitHashClientStructs)</_Parameter2>
|
||||
|
|
@ -215,9 +231,4 @@
|
|||
<!-- writes the attribute to the customAssemblyInfo file -->
|
||||
<WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
|
||||
</Target>
|
||||
|
||||
<!-- Copy plugin .targets folder into distrib -->
|
||||
<Target Name="CopyPluginTargets" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(ProjectDir)\..\targets\Dalamud.Plugin.targets;$(ProjectDir)\..\targets\Dalamud.Plugin.Bootstrap.targets" DestinationFolder="$(OutDir)\targets" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public enum DalamudAsset
|
|||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "troubleIcon.png")]
|
||||
TroubleIcon = 1006,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
|
||||
/// </summary>
|
||||
|
|
@ -124,6 +124,13 @@ public enum DalamudAsset
|
|||
[DalamudAssetPath("UIRes", "tsmShade.png")]
|
||||
TitleScreenMenuShade = 1013,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: Atlas containing badges.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "badgeAtlas.png")]
|
||||
BadgeAtlas = 1015,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
|
||||
/// </summary>
|
||||
|
|
@ -151,7 +158,7 @@ public enum DalamudAsset
|
|||
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.Font)]
|
||||
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
|
||||
[DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")]
|
||||
FontAwesomeFreeSolid = 2003,
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
|
||||
using Lumina;
|
||||
using Lumina.Data;
|
||||
using Lumina.Excel;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Data;
|
||||
|
|
@ -41,7 +43,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
try
|
||||
{
|
||||
Log.Verbose("Starting data load...");
|
||||
|
||||
|
||||
using (Timings.Start("Lumina Init"))
|
||||
{
|
||||
var luminaOptions = new LuminaOptions
|
||||
|
|
@ -53,12 +55,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
DefaultExcelLanguage = this.Language.ToLumina(),
|
||||
};
|
||||
|
||||
this.GameData = new(
|
||||
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
|
||||
luminaOptions)
|
||||
try
|
||||
{
|
||||
StreamPool = new(),
|
||||
};
|
||||
this.GameData = new(
|
||||
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
|
||||
luminaOptions)
|
||||
{
|
||||
StreamPool = new(),
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Lumina GameData init failed");
|
||||
Util.Fatal(
|
||||
"Dalamud could not read required game data files. This likely means your game installation is corrupted or incomplete.\n\n" +
|
||||
"Please repair your installation by right-clicking the login button in XIVLauncher and choosing \"Repair game files\".",
|
||||
"Dalamud");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
|
||||
|
||||
|
|
@ -69,9 +84,14 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
var tsInfo =
|
||||
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
|
||||
dalamud.StartInfo.TroubleshootingPackData);
|
||||
this.HasModifiedGameDataFiles =
|
||||
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
|
||||
|
||||
|
||||
// Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout
|
||||
// this.HasModifiedGameDataFiles =
|
||||
// tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
|
||||
|
||||
// TODO: Put above back when check in XL is fixed
|
||||
this.HasModifiedGameDataFiles = false;
|
||||
|
||||
if (this.HasModifiedGameDataFiles)
|
||||
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);
|
||||
}
|
||||
|
|
@ -130,7 +150,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
#region Lumina Wrappers
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
|
||||
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
|
||||
=> this.Excel.GetSheet<T>(language?.ToLumina(), name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -138,7 +158,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
=> this.Excel.GetSubrowSheet<T>(language?.ToLumina(), name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FileResource? GetFile(string path)
|
||||
public FileResource? GetFile(string path)
|
||||
=> this.GetFile<FileResource>(path);
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -161,7 +181,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
: Task.FromException<T>(new FileNotFoundException("The file could not be found."));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool FileExists(string path)
|
||||
public bool FileExists(string path)
|
||||
=> this.GameData.FileExists(path);
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ using System.Collections.Generic;
|
|||
using Dalamud.Hooking;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
|
||||
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace Dalamud.Data;
|
||||
|
|
@ -13,7 +15,7 @@ namespace Dalamud.Data;
|
|||
/// </summary>
|
||||
internal sealed unsafe class RsvResolver : IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new("RsvProvider");
|
||||
private static readonly ModuleLog Log = ModuleLog.Create<RsvResolver>();
|
||||
|
||||
private readonly Hook<LayoutWorld.Delegates.AddRsvString> addRsvStringHook;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -16,10 +14,13 @@ using Dalamud.Plugin.Internal;
|
|||
using Dalamud.Storage;
|
||||
using Dalamud.Support;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
|
|
@ -144,7 +145,8 @@ public sealed class EntryPoint
|
|||
|
||||
// Load configuration first to get some early persistent state, like log level
|
||||
var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!);
|
||||
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs);
|
||||
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// Set the appropriate logging level from the configuration
|
||||
if (!configuration.LogSynchronously)
|
||||
|
|
@ -191,8 +193,8 @@ public sealed class EntryPoint
|
|||
|
||||
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
|
||||
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
|
||||
Util.GetScmVersion(),
|
||||
Util.GetGitHashClientStructs(),
|
||||
Versioning.GetScmVersion(),
|
||||
Versioning.GetGitHashClientStructs(),
|
||||
FFXIVClientStructs.ThisAssembly.Git.Commits);
|
||||
|
||||
dalamud.WaitForUnload();
|
||||
|
|
@ -262,7 +264,7 @@ public sealed class EntryPoint
|
|||
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
|
||||
var searchPath = $".;{symbolPath}";
|
||||
|
||||
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle();
|
||||
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess();
|
||||
|
||||
// Remove any existing Symbol Handler and Init a new one with our search path added
|
||||
Windows.Win32.PInvoke.SymCleanup(currentProcess);
|
||||
|
|
@ -291,7 +293,6 @@ public sealed class EntryPoint
|
|||
}
|
||||
|
||||
var pluginInfo = string.Empty;
|
||||
var supportText = ", please visit us on Discord for more help";
|
||||
try
|
||||
{
|
||||
var pm = Service<PluginManager>.GetNullable();
|
||||
|
|
@ -299,9 +300,6 @@ public sealed class EntryPoint
|
|||
if (plugin != null)
|
||||
{
|
||||
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
|
||||
|
||||
if (plugin.IsThirdParty)
|
||||
supportText = string.Empty;
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
|
@ -309,31 +307,18 @@ public sealed class EntryPoint
|
|||
// ignored
|
||||
}
|
||||
|
||||
const MESSAGEBOX_STYLE flags = MESSAGEBOX_STYLE.MB_YESNO | MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_SYSTEMMODAL;
|
||||
var result = Windows.Win32.PInvoke.MessageBox(
|
||||
new HWND(Process.GetCurrentProcess().MainWindowHandle),
|
||||
$"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\n{ex.GetType().Name}\n{info}\n\n{pluginInfo}More information has been recorded separately{supportText}.\n\nDo you want to disable all plugins the next time you start the game?",
|
||||
"Dalamud",
|
||||
flags);
|
||||
|
||||
if (result == MESSAGEBOX_RESULT.IDYES)
|
||||
{
|
||||
Log.Information("User chose to disable plugins on next launch...");
|
||||
var config = Service<DalamudConfiguration>.Get();
|
||||
config.PluginSafeMode = true;
|
||||
config.ForceSave();
|
||||
}
|
||||
|
||||
Log.CloseAndFlush();
|
||||
Environment.Exit(-1);
|
||||
|
||||
ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}");
|
||||
break;
|
||||
default:
|
||||
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
|
||||
|
||||
Log.CloseAndFlush();
|
||||
Environment.Exit(-1);
|
||||
break;
|
||||
}
|
||||
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
|
||||
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)
|
||||
|
|
|
|||
3
Dalamud/EnumCloneMap.txt
Normal file
3
Dalamud/EnumCloneMap.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Format: Target.Full.TypeName = Source.Full.EnumTypeName
|
||||
# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum
|
||||
Dalamud.Game.Agent.AgentId = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
namespace Dalamud.Game.Addon;
|
||||
|
||||
/// <summary>Argument pool for Addon Lifecycle services.</summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class AddonLifecyclePooledArgs : IServiceType
|
||||
{
|
||||
private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
|
||||
private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
|
||||
private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
|
||||
private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
|
||||
private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
|
||||
private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
|
||||
private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecyclePooledArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
|
||||
new(out arg, this.addonRequestedUpdateArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
|
||||
new(out arg, this.addonReceiveEventArgPool);
|
||||
|
||||
/// <summary>Returns the object to the pool on dispose.</summary>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
public readonly ref struct PooledEntry<T>
|
||||
where T : AddonArgs, new()
|
||||
{
|
||||
private readonly Span<T> pool;
|
||||
private readonly T obj;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
|
||||
/// <param name="arg">An instance of the argument.</param>
|
||||
/// <param name="pool">The pool to rent from and return to.</param>
|
||||
public PooledEntry(out T arg, Span<T> pool)
|
||||
{
|
||||
this.pool = pool;
|
||||
foreach (ref var item in pool)
|
||||
{
|
||||
if (Interlocked.Exchange(ref item, null) is { } v)
|
||||
{
|
||||
this.obj = arg = v;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.obj = arg = new();
|
||||
}
|
||||
|
||||
/// <summary>Returns the item to the pool.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
var tmp = this.obj;
|
||||
foreach (ref var item in this.pool)
|
||||
{
|
||||
if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
|
||||
return;
|
||||
tmp = tmp2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using Dalamud.Logging.Internal;
|
|||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Events;
|
||||
|
|
@ -25,32 +24,28 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
/// </summary>
|
||||
public static readonly Guid DalamudInternalKey = Guid.NewGuid();
|
||||
|
||||
private static readonly ModuleLog Log = new("AddonEventManager");
|
||||
private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
|
||||
|
||||
private readonly AddonLifecycleEventListener finalizeEventListener;
|
||||
|
||||
private readonly AddonEventManagerAddressResolver address;
|
||||
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
|
||||
private readonly Hook<AtkUnitManager.Delegates.UpdateCursor> onUpdateCursor;
|
||||
|
||||
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
|
||||
|
||||
private AddonCursorType? cursorOverride;
|
||||
private AtkCursor.CursorType? cursorOverride;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonEventManager(TargetSigScanner sigScanner)
|
||||
private AddonEventManager()
|
||||
{
|
||||
this.address = new AddonEventManagerAddressResolver();
|
||||
this.address.Setup(sigScanner);
|
||||
|
||||
this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>();
|
||||
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
|
||||
|
||||
this.cursorOverride = null;
|
||||
|
||||
this.onUpdateCursor = Hook<UpdateCursorDelegate>.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
|
||||
this.onUpdateCursor = Hook<AtkUnitManager.Delegates.UpdateCursor>.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour);
|
||||
|
||||
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
|
||||
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
|
||||
|
|
@ -58,8 +53,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
this.onUpdateCursor.Enable();
|
||||
}
|
||||
|
||||
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
|
|
@ -117,7 +110,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
/// Force the game cursor to be the specified cursor.
|
||||
/// </summary>
|
||||
/// <param name="cursor">Which cursor to use.</param>
|
||||
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
|
||||
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor;
|
||||
|
||||
/// <summary>
|
||||
/// Un-forces the game cursor.
|
||||
|
|
@ -168,7 +161,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
}
|
||||
}
|
||||
|
||||
private nint UpdateCursorDetour(RaptureAtkModule* module)
|
||||
private void UpdateCursorDetour(AtkUnitManager* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -176,13 +169,14 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
|
||||
if (this.cursorOverride is not null && atkStage is not null)
|
||||
{
|
||||
var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
|
||||
if (cursor != this.cursorOverride)
|
||||
ref var atkCursor = ref atkStage->AtkCursor;
|
||||
|
||||
if (atkCursor.Type != this.cursorOverride)
|
||||
{
|
||||
AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
|
||||
atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -190,7 +184,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
Log.Error(e, "Exception in UpdateCursorDetour.");
|
||||
}
|
||||
|
||||
return this.onUpdateCursor!.Original(module);
|
||||
this.onUpdateCursor!.Original(thisPtr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
namespace Dalamud.Game.Addon.Events;
|
||||
|
||||
/// <summary>
|
||||
/// AddonEventManager memory address resolver.
|
||||
/// </summary>
|
||||
internal class AddonEventManagerAddressResolver : BaseAddressResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the AtkModule UpdateCursor method.
|
||||
/// </summary>
|
||||
public nint UpdateCursor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan for and setup any configured address pointers.
|
||||
/// </summary>
|
||||
/// <param name="scanner">The signature scanner to facilitate setup.</param>
|
||||
protected override void Setup64Bit(ISigScanner scanner)
|
||||
{
|
||||
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,11 @@ public enum AddonEventType : byte
|
|||
/// </summary>
|
||||
InputBaseInputReceived = 15,
|
||||
|
||||
/// <summary>
|
||||
/// Fired at the very beginning of AtkInputManager.HandleInput on AtkStage.ViewportEventManager. Used in LovmMiniMap.
|
||||
/// </summary>
|
||||
RawInputData = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Focus Start.
|
||||
/// </summary>
|
||||
|
|
@ -74,7 +79,7 @@ public enum AddonEventType : byte
|
|||
/// <summary>
|
||||
/// Resize (ChatLogPanel).
|
||||
/// </summary>
|
||||
Resize = 19,
|
||||
Resize = 21,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentButton Press, sent on MouseDown on Button.
|
||||
|
|
@ -107,7 +112,12 @@ public enum AddonEventType : byte
|
|||
SliderReleased = 30,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentList RollOver.
|
||||
/// AtkComponentList Button Press.
|
||||
/// </summary>
|
||||
ListButtonPress = 31,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentList Roll Over.
|
||||
/// </summary>
|
||||
ListItemRollOver = 33,
|
||||
|
||||
|
|
@ -126,11 +136,31 @@ public enum AddonEventType : byte
|
|||
/// </summary>
|
||||
ListItemDoubleClick = 36,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentList Highlight.
|
||||
/// </summary>
|
||||
ListItemHighlight = 37,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentList Select.
|
||||
/// </summary>
|
||||
ListItemSelect = 38,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentList Pad Drag Drop Begin.
|
||||
/// </summary>
|
||||
ListItemPadDragDropBegin = 40,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentList Pad Drag Drop End.
|
||||
/// </summary>
|
||||
ListItemPadDragDropEnd = 41,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentList Pad Drag Drop Insert.
|
||||
/// </summary>
|
||||
ListItemPadDragDropInsert = 42,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentDragDrop Begin.
|
||||
/// Sent on MouseDown over a draggable icon (will NOT send for a locked icon).
|
||||
|
|
@ -142,12 +172,22 @@ public enum AddonEventType : byte
|
|||
/// </summary>
|
||||
DragDropEnd = 51,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentDragDrop Insert Attempt.
|
||||
/// </summary>
|
||||
DragDropInsertAttempt = 52,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentDragDrop Insert.
|
||||
/// Sent when dropping an icon into a hotbar/inventory slot or similar.
|
||||
/// </summary>
|
||||
DragDropInsert = 53,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentDragDrop Can Accept Check.
|
||||
/// </summary>
|
||||
DragDropCanAcceptCheck = 54,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentDragDrop Roll Over.
|
||||
/// </summary>
|
||||
|
|
@ -165,23 +205,18 @@ public enum AddonEventType : byte
|
|||
DragDropDiscard = 57,
|
||||
|
||||
/// <summary>
|
||||
/// Drag Drop Unknown.
|
||||
/// AtkComponentDragDrop Click.
|
||||
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
|
||||
/// </summary>
|
||||
[Obsolete("Use DragDropDiscard", true)]
|
||||
DragDropUnk54 = 54,
|
||||
DragDropClick = 58,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentDragDrop Cancel.
|
||||
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
|
||||
/// </summary>
|
||||
[Obsolete("Renamed to DragDropClick")]
|
||||
DragDropCancel = 58,
|
||||
|
||||
/// <summary>
|
||||
/// Drag Drop Unknown.
|
||||
/// </summary>
|
||||
[Obsolete("Use DragDropCancel", true)]
|
||||
DragDropUnk55 = 55,
|
||||
|
||||
/// <summary>
|
||||
/// AtkComponentIconText Roll Over.
|
||||
/// </summary>
|
||||
|
|
@ -217,6 +252,11 @@ public enum AddonEventType : byte
|
|||
/// </summary>
|
||||
TimerEnd = 65,
|
||||
|
||||
/// <summary>
|
||||
/// AtkTimer Start.
|
||||
/// </summary>
|
||||
TimerStart = 66,
|
||||
|
||||
/// <summary>
|
||||
/// AtkSimpleTween Progress.
|
||||
/// </summary>
|
||||
|
|
@ -247,6 +287,11 @@ public enum AddonEventType : byte
|
|||
/// </summary>
|
||||
WindowChangeScale = 72,
|
||||
|
||||
/// <summary>
|
||||
/// AtkTimeline Active Label Changed.
|
||||
/// </summary>
|
||||
TimelineActiveLabelChanged = 75,
|
||||
|
||||
/// <summary>
|
||||
/// AtkTextNode Link Mouse Click.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using Dalamud.Game.Addon.Events.EventDataTypes;
|
|||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
|
|
@ -16,7 +15,7 @@ namespace Dalamud.Game.Addon.Events;
|
|||
/// </summary>
|
||||
internal unsafe class PluginEventController : IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new("AddonEventManager");
|
||||
private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginEventController"/> class.
|
||||
|
|
@ -28,7 +27,7 @@ internal unsafe class PluginEventController : IDisposable
|
|||
|
||||
private AddonEventListener EventListener { get; init; }
|
||||
|
||||
private List<AddonEventEntry> Events { get; } = new();
|
||||
private List<AddonEventEntry> Events { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a tracked event.
|
||||
|
|
|
|||
|
|
@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||
/// <summary>
|
||||
/// Base class for AddonLifecycle AddonArgTypes.
|
||||
/// </summary>
|
||||
public abstract unsafe class AddonArgs
|
||||
public class AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant string representing the name of an addon that is invalid.
|
||||
/// </summary>
|
||||
public const string InvalidAddon = "NullAddon";
|
||||
|
||||
private string? addonName;
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon this args referrers to.
|
||||
/// </summary>
|
||||
public string AddonName => this.GetAddonName();
|
||||
public string AddonName { get; private set; } = InvalidAddon;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pointer to the addons AtkUnitBase.
|
||||
|
|
@ -25,55 +30,17 @@ public abstract unsafe class AddonArgs
|
|||
public AtkUnitBasePtr Addon
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
internal set
|
||||
{
|
||||
field = value;
|
||||
|
||||
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
|
||||
this.AddonName = value.Name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of these args.
|
||||
/// </summary>
|
||||
public abstract AddonArgsType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if addon name matches the given span of char.
|
||||
/// </summary>
|
||||
/// <param name="name">The name to check.</param>
|
||||
/// <returns>Whether it is the case.</returns>
|
||||
internal bool IsAddon(string name)
|
||||
{
|
||||
if (this.Addon.IsNull)
|
||||
return false;
|
||||
|
||||
if (name.Length is 0 or > 32)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(this.Addon.Name))
|
||||
return false;
|
||||
|
||||
return name == this.Addon.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears this AddonArgs values.
|
||||
/// </summary>
|
||||
internal virtual void Clear()
|
||||
{
|
||||
this.addonName = null;
|
||||
this.Addon = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for ensuring the name of the addon is valid.
|
||||
/// </summary>
|
||||
/// <returns>The name of the addon for this object. <see cref="InvalidAddon"/> when invalid.</returns>
|
||||
private string GetAddonName()
|
||||
{
|
||||
if (this.Addon.IsNull) return InvalidAddon;
|
||||
|
||||
var name = this.Addon.Name;
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return InvalidAddon;
|
||||
|
||||
return this.addonName ??= name;
|
||||
}
|
||||
public virtual AddonArgsType Type => AddonArgsType.Generic;
|
||||
}
|
||||
|
|
|
|||
22
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs
Normal file
22
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Close events.
|
||||
/// </summary>
|
||||
public class AddonCloseArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonCloseArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonCloseArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Close;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window should fire the callback method on close.
|
||||
/// </summary>
|
||||
public bool FireCallback { get; set; }
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Draw events.
|
||||
/// </summary>
|
||||
public class AddonDrawArgs : AddonArgs, ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonDrawArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Draw;
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for ReceiveEvent events.
|
||||
/// </summary>
|
||||
public class AddonFinalizeArgs : AddonArgs, ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonFinalizeArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Finalize;
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for OnFocusChanged events.
|
||||
/// </summary>
|
||||
public class AddonFocusChangedArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonFocusChangedArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonFocusChangedArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.FocusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window is being focused or unfocused.
|
||||
/// </summary>
|
||||
public bool ShouldFocus { get; set; }
|
||||
}
|
||||
32
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs
Normal file
32
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Hide events.
|
||||
/// </summary>
|
||||
public class AddonHideArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonHideArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonHideArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Hide;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to call the hide callback handler when this hides.
|
||||
/// </summary>
|
||||
public bool CallHideCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flags that the window will set when it Shows/Hides.
|
||||
/// </summary>
|
||||
public uint SetShowHideFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether something for this event message.
|
||||
/// </summary>
|
||||
internal bool UnknownBool { get; set; }
|
||||
}
|
||||
|
|
@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||
/// <summary>
|
||||
/// Addon argument data for ReceiveEvent events.
|
||||
/// </summary>
|
||||
public class AddonReceiveEventArgs : AddonArgs, ICloneable
|
||||
public class AddonReceiveEventArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonReceiveEventArgs()
|
||||
internal AddonReceiveEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -32,23 +31,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
|
|||
public nint AtkEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pointer to a block of data for this event message.
|
||||
/// Gets or sets the pointer to an AtkEventData for this event message.
|
||||
/// </summary>
|
||||
public nint Data { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.AtkEventType = default;
|
||||
this.EventParam = default;
|
||||
this.AtkEvent = default;
|
||||
this.Data = default;
|
||||
}
|
||||
public nint AtkEventData { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.NativeWrapper;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Refresh events.
|
||||
/// </summary>
|
||||
public class AddonRefreshArgs : AddonArgs, ICloneable
|
||||
public class AddonRefreshArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonRefreshArgs()
|
||||
internal AddonRefreshArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -31,19 +36,32 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
|
|||
/// <summary>
|
||||
/// Gets the AtkValues in the form of a span.
|
||||
/// </summary>
|
||||
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
|
||||
[Api15ToDo("Make this internal, remove obsolete")]
|
||||
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
/// <summary>
|
||||
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
|
||||
/// </returns>
|
||||
public IEnumerable<AtkValuePtr> AtkValueEnumerable
|
||||
{
|
||||
base.Clear();
|
||||
this.AtkValueCount = default;
|
||||
this.AtkValues = default;
|
||||
get
|
||||
{
|
||||
for (var i = 0; i < this.AtkValueCount; i++)
|
||||
{
|
||||
AtkValuePtr ptr;
|
||||
unsafe
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
yield return ptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||
/// <summary>
|
||||
/// Addon argument data for OnRequestedUpdate events.
|
||||
/// </summary>
|
||||
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
|
||||
public class AddonRequestedUpdateArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonRequestedUpdateArgs()
|
||||
internal AddonRequestedUpdateArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -25,18 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
|
|||
/// Gets or sets the StringArrayData** for this event.
|
||||
/// </summary>
|
||||
public nint StringArrayData { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.NumberArrayData = default;
|
||||
this.StringArrayData = default;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.NativeWrapper;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Setup events.
|
||||
/// </summary>
|
||||
public class AddonSetupArgs : AddonArgs, ICloneable
|
||||
public class AddonSetupArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonSetupArgs()
|
||||
internal AddonSetupArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -31,19 +36,32 @@ public class AddonSetupArgs : AddonArgs, ICloneable
|
|||
/// <summary>
|
||||
/// Gets the AtkValues in the form of a span.
|
||||
/// </summary>
|
||||
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
|
||||
[Api15ToDo("Make this internal, remove obsolete")]
|
||||
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
/// <summary>
|
||||
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
|
||||
/// </returns>
|
||||
public IEnumerable<AtkValuePtr> AtkValueEnumerable
|
||||
{
|
||||
base.Clear();
|
||||
this.AtkValueCount = default;
|
||||
this.AtkValues = default;
|
||||
get
|
||||
{
|
||||
for (var i = 0; i < this.AtkValueCount; i++)
|
||||
{
|
||||
AtkValuePtr ptr;
|
||||
unsafe
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
yield return ptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs
Normal file
27
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Show events.
|
||||
/// </summary>
|
||||
public class AddonShowArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonShowArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonShowArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Show;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window should play open sound effects.
|
||||
/// </summary>
|
||||
public bool SilenceOpenSoundEffect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flags that the window will unset when it Shows/Hides.
|
||||
/// </summary>
|
||||
public uint UnsetShowHideFlags { get; set; }
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Update events.
|
||||
/// </summary>
|
||||
public class AddonUpdateArgs : AddonArgs, ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonUpdateArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Update;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time since the last update.
|
||||
/// </summary>
|
||||
public float TimeDelta
|
||||
{
|
||||
get => this.TimeDeltaInternal;
|
||||
init => this.TimeDeltaInternal = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time since the last update.
|
||||
/// </summary>
|
||||
internal float TimeDeltaInternal { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.TimeDeltaInternal = default;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,38 +5,48 @@
|
|||
/// </summary>
|
||||
public enum AddonArgsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic arg type that contains no meaningful data.
|
||||
/// </summary>
|
||||
Generic,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Setup.
|
||||
/// </summary>
|
||||
Setup,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Update.
|
||||
/// </summary>
|
||||
Update,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Draw.
|
||||
/// </summary>
|
||||
Draw,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Finalize.
|
||||
/// </summary>
|
||||
Finalize,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for RequestedUpdate.
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
RequestedUpdate,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Refresh.
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
Refresh,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for ReceiveEvent.
|
||||
/// </summary>
|
||||
ReceiveEvent,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Show.
|
||||
/// </summary>
|
||||
Show,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Hide.
|
||||
/// </summary>
|
||||
Hide,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Close.
|
||||
/// </summary>
|
||||
Close,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for OnFocusChanged.
|
||||
/// </summary>
|
||||
FocusChanged,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public enum AddonEvent
|
|||
/// </summary>
|
||||
/// <seealso cref="AddonSetupArgs"/>
|
||||
PreSetup,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for
|
||||
/// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
|
||||
|
|
@ -29,7 +29,6 @@ public enum AddonEvent
|
|||
/// An event that is fired before an addon begins its update cycle via <see cref="AtkUnitBase.Update"/>. This event
|
||||
/// is fired every frame that an addon is loaded, regardless of visibility.
|
||||
/// </summary>
|
||||
/// <seealso cref="AddonUpdateArgs"/>
|
||||
PreUpdate,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -42,7 +41,6 @@ public enum AddonEvent
|
|||
/// An event that is fired before an addon begins drawing to screen via <see cref="AtkUnitBase.Draw"/>. Unlike
|
||||
/// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen.
|
||||
/// </summary>
|
||||
/// <seealso cref="AddonDrawArgs"/>
|
||||
PreDraw,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -62,9 +60,8 @@ public enum AddonEvent
|
|||
/// <br />
|
||||
/// As this is part of the destruction process for an addon, this event does not have an associated Post event.
|
||||
/// </remarks>
|
||||
/// <seealso cref="AddonFinalizeArgs"/>
|
||||
PreFinalize,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before a call to <see cref="AtkUnitBase.OnRequestedUpdate"/> is made in response to a
|
||||
/// change in the subscribed <see cref="AddonRequestedUpdateArgs.NumberArrayData"/> or
|
||||
|
|
@ -81,13 +78,13 @@ public enum AddonEvent
|
|||
/// to the Free Company's overview.
|
||||
/// </example>
|
||||
PreRequestedUpdate,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has finished processing an <c>ArrayData</c> update.
|
||||
/// See <see cref="PreRequestedUpdate"/> for more information.
|
||||
/// </summary>
|
||||
PostRequestedUpdate,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon calls its <see cref="AtkUnitManager.RefreshAddon"/> method. Refreshes are
|
||||
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
|
||||
|
|
@ -96,13 +93,13 @@ public enum AddonEvent
|
|||
/// <seealso cref="AddonRefreshArgs"/>
|
||||
/// <seealso cref="PostRefresh"/>
|
||||
PreRefresh,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has finished its refresh.
|
||||
/// See <see cref="PreRefresh"/> for more information.
|
||||
/// </summary>
|
||||
PostRefresh,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon begins processing a user-driven event via
|
||||
/// <see cref="AtkEventListener.ReceiveEvent"/>, such as mousing over an element or clicking a button. This event
|
||||
|
|
@ -112,10 +109,108 @@ public enum AddonEvent
|
|||
/// <seealso cref="AddonReceiveEventArgs"/>
|
||||
/// <seealso cref="PostReceiveEvent"/>
|
||||
PreReceiveEvent,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method.
|
||||
/// See <see cref="PreReceiveEvent"/> for more information.
|
||||
/// </summary>
|
||||
PostReceiveEvent,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its open method.
|
||||
/// </summary>
|
||||
PreOpen,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its open method.
|
||||
/// </summary>
|
||||
PostOpen,
|
||||
|
||||
/// <summary>
|
||||
/// An even that is fired before an addon processes its Close method.
|
||||
/// </summary>
|
||||
PreClose,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Close method.
|
||||
/// </summary>
|
||||
PostClose,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its Show method.
|
||||
/// </summary>
|
||||
PreShow,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Show method.
|
||||
/// </summary>
|
||||
PostShow,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its Hide method.
|
||||
/// </summary>
|
||||
PreHide,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Hide method.
|
||||
/// </summary>
|
||||
PostHide,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its OnMove method.
|
||||
/// OnMove is triggered only when a move is completed.
|
||||
/// </summary>
|
||||
PreMove,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its OnMove method.
|
||||
/// OnMove is triggered only when a move is completed.
|
||||
/// </summary>
|
||||
PostMove,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its MouseOver method.
|
||||
/// </summary>
|
||||
PreMouseOver,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its MouseOver method.
|
||||
/// </summary>
|
||||
PostMouseOver,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its MouseOut method.
|
||||
/// </summary>
|
||||
PreMouseOut,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its MouseOut method.
|
||||
/// </summary>
|
||||
PostMouseOut,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its Focus method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
|
||||
/// </remarks>
|
||||
PreFocus,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Focus method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
|
||||
/// </remarks>
|
||||
PostFocus,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its FocusChanged method.
|
||||
/// </summary>
|
||||
PreFocusChanged,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after a addon processes its FocusChanged method.
|
||||
/// </summary>
|
||||
PostFocusChanged,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Hooking.Internal;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
|
@ -21,75 +20,56 @@ namespace Dalamud.Game.Addon.Lifecycle;
|
|||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class AddonLifecycle : IInternalDisposableService
|
||||
{
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
/// <summary>
|
||||
/// Gets a list of all allocated addon virtual tables.
|
||||
/// </summary>
|
||||
public static readonly List<AddonVirtualTable> AllocatedTables = [];
|
||||
|
||||
private static readonly ModuleLog Log = ModuleLog.Create<AddonLifecycle>();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
|
||||
|
||||
private readonly nint disallowedReceiveEventAddress;
|
||||
|
||||
private readonly AddonLifecycleAddressResolver address;
|
||||
private readonly AddonSetupHook<AtkUnitBase.Delegates.OnSetup> onAddonSetupHook;
|
||||
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
|
||||
private readonly CallHook<AtkUnitBase.Delegates.Draw> onAddonDrawHook;
|
||||
private readonly CallHook<AtkUnitBase.Delegates.Update> onAddonUpdateHook;
|
||||
private readonly Hook<AtkUnitManager.Delegates.RefreshAddon> onAddonRefreshHook;
|
||||
private readonly CallHook<AtkUnitBase.Delegates.OnRequestedUpdate> onAddonRequestedUpdateHook;
|
||||
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
|
||||
private bool isInvokingListeners;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecycle(TargetSigScanner sigScanner)
|
||||
private AddonLifecycle()
|
||||
{
|
||||
this.address = new AddonLifecycleAddressResolver();
|
||||
this.address.Setup(sigScanner);
|
||||
|
||||
this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
|
||||
|
||||
var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
|
||||
|
||||
this.onAddonSetupHook = new AddonSetupHook<AtkUnitBase.Delegates.OnSetup>(this.address.AddonSetup, this.OnAddonSetup);
|
||||
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
|
||||
this.onAddonDrawHook = new CallHook<AtkUnitBase.Delegates.Draw>(this.address.AddonDraw, this.OnAddonDraw);
|
||||
this.onAddonUpdateHook = new CallHook<AtkUnitBase.Delegates.Update>(this.address.AddonUpdate, this.OnAddonUpdate);
|
||||
this.onAddonRefreshHook = Hook<AtkUnitManager.Delegates.RefreshAddon>.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
|
||||
this.onAddonRequestedUpdateHook = new CallHook<AtkUnitBase.Delegates.OnRequestedUpdate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
|
||||
|
||||
this.onAddonSetupHook.Enable();
|
||||
this.onAddonFinalizeHook.Enable();
|
||||
this.onAddonDrawHook.Enable();
|
||||
this.onAddonUpdateHook.Enable();
|
||||
this.onAddonRefreshHook.Enable();
|
||||
this.onAddonRequestedUpdateHook.Enable();
|
||||
this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
|
||||
this.onInitializeAddonHook.Enable();
|
||||
}
|
||||
|
||||
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
|
||||
/// </summary>
|
||||
internal List<AddonLifecycleReceiveEventListener> ReceiveEventListeners { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all AddonLifecycle Event Listeners.
|
||||
/// </summary>
|
||||
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
|
||||
/// </summary> <br/>
|
||||
/// Mapping is: EventType -> AddonName -> ListenerList
|
||||
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.onAddonSetupHook.Dispose();
|
||||
this.onAddonFinalizeHook.Dispose();
|
||||
this.onAddonDrawHook.Dispose();
|
||||
this.onAddonUpdateHook.Dispose();
|
||||
this.onAddonRefreshHook.Dispose();
|
||||
this.onAddonRequestedUpdateHook.Dispose();
|
||||
this.onInitializeAddonHook?.Dispose();
|
||||
this.onInitializeAddonHook = null;
|
||||
|
||||
foreach (var receiveEventListener in this.ReceiveEventListeners)
|
||||
AllocatedTables.ForEach(entry => entry.Dispose());
|
||||
AllocatedTables.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a virtual table address to the original virtual table address.
|
||||
/// </summary>
|
||||
/// <param name="tableAddress">The modified address to resolve.</param>
|
||||
/// <returns>The original address.</returns>
|
||||
internal static AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
|
||||
{
|
||||
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
|
||||
if (matchedTable == null)
|
||||
{
|
||||
receiveEventListener.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
return matchedTable.OriginalVirtualTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -98,20 +78,14 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
|
|||
/// <param name="listener">The listener to register.</param>
|
||||
internal void RegisterListener(AddonLifecycleEventListener listener)
|
||||
{
|
||||
this.framework.RunOnTick(() =>
|
||||
if (this.isInvokingListeners)
|
||||
{
|
||||
this.EventListeners.Add(listener);
|
||||
|
||||
// If we want receive event messages have an already active addon, enable the receive event hook.
|
||||
// If the addon isn't active yet, we'll grab the hook when it sets up.
|
||||
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
||||
{
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
|
||||
{
|
||||
receiveEventListener.TryEnable();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -120,27 +94,16 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
|
|||
/// <param name="listener">The listener to unregister.</param>
|
||||
internal void UnregisterListener(AddonLifecycleEventListener listener)
|
||||
{
|
||||
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
|
||||
listener.Removed = true;
|
||||
listener.IsRequestedToClear = true;
|
||||
|
||||
this.framework.RunOnTick(() =>
|
||||
if (this.isInvokingListeners)
|
||||
{
|
||||
this.EventListeners.Remove(listener);
|
||||
|
||||
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
|
||||
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
||||
{
|
||||
// Get the ReceiveEvent Listener for this addon
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
|
||||
{
|
||||
// If there are no other listeners listening for this event, disable the hook.
|
||||
if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
|
||||
{
|
||||
receiveEventListener.Disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -151,226 +114,104 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
|
|||
/// <param name="blame">What to blame on errors.</param>
|
||||
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
|
||||
{
|
||||
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
|
||||
foreach (var listener in this.EventListeners)
|
||||
this.isInvokingListeners = true;
|
||||
|
||||
// Early return if we don't have any listeners of this type
|
||||
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
|
||||
|
||||
// Handle listeners for this event type that don't care which addon is triggering it
|
||||
if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
|
||||
{
|
||||
if (listener.EventType != eventType)
|
||||
continue;
|
||||
|
||||
// If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
|
||||
if (listener.Removed)
|
||||
continue;
|
||||
|
||||
// Match on string.empty for listeners that want events for all addons.
|
||||
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
|
||||
continue;
|
||||
|
||||
try
|
||||
foreach (var listener in globalListeners)
|
||||
{
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterReceiveEventHook(AtkUnitBase* addon)
|
||||
{
|
||||
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
|
||||
// Disallows hooking the core internal event handler.
|
||||
var addonName = addon->NameString;
|
||||
var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
|
||||
if (receiveEventAddress != this.disallowedReceiveEventAddress)
|
||||
{
|
||||
// If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
|
||||
{
|
||||
if (!existingListener.AddonNames.Contains(addonName))
|
||||
if (listener.IsRequestedToClear) continue;
|
||||
|
||||
try
|
||||
{
|
||||
existingListener.AddonNames.Add(addonName);
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
}
|
||||
|
||||
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
|
||||
else
|
||||
{
|
||||
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
|
||||
}
|
||||
|
||||
// If we have an active listener for this addon already, we need to activate this hook.
|
||||
if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
|
||||
{
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
|
||||
catch (Exception e)
|
||||
{
|
||||
receiveEventListener.TryEnable();
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle listeners that are listening for this addon and event type specifically
|
||||
if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
|
||||
{
|
||||
foreach (var listener in addonListener)
|
||||
{
|
||||
if (listener.IsRequestedToClear) continue;
|
||||
|
||||
try
|
||||
{
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isInvokingListeners = false;
|
||||
}
|
||||
|
||||
private void UnregisterReceiveEventHook(string addonName)
|
||||
private void RegisterListenerMethod(AddonLifecycleEventListener listener)
|
||||
{
|
||||
// Remove this addons ReceiveEvent Registration
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
|
||||
if (!this.EventListeners.ContainsKey(listener.EventType))
|
||||
{
|
||||
eventListener.AddonNames.Remove(addonName);
|
||||
|
||||
// If there are no more listeners let's remove and dispose.
|
||||
if (eventListener.AddonNames.Count is 0)
|
||||
if (!this.EventListeners.TryAdd(listener.EventType, []))
|
||||
{
|
||||
this.ReceiveEventListeners.Remove(eventListener);
|
||||
eventListener.Dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
|
||||
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
|
||||
{
|
||||
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
|
||||
}
|
||||
|
||||
private void UnregisterListenerMethod(AddonLifecycleEventListener listener)
|
||||
{
|
||||
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
|
||||
{
|
||||
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
|
||||
{
|
||||
addonListener.Remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
private void OnAddonInitialize(AtkUnitBase* addon)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.RegisterReceiveEventHook(addon);
|
||||
this.LogInitialize(addon->NameString);
|
||||
|
||||
// AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
|
||||
AllocatedTables.Add(new AddonVirtualTable(addon, this));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
|
||||
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
|
||||
}
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.AtkValueCount = valueCount;
|
||||
arg.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
|
||||
valueCount = arg.AtkValueCount;
|
||||
values = (AtkValue*)arg.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
addon->OnSetup(valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
|
||||
this.onInitializeAddonHook!.Original(addon);
|
||||
}
|
||||
|
||||
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
|
||||
[Conditional("DEBUG")]
|
||||
private void LogInitialize(string addonName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addonName = atkUnitBase[0]->NameString;
|
||||
this.UnregisterReceiveEventHook(addonName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
|
||||
}
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)atkUnitBase[0];
|
||||
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
|
||||
|
||||
try
|
||||
{
|
||||
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonDraw(AtkUnitBase* addon)
|
||||
{
|
||||
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
|
||||
|
||||
try
|
||||
{
|
||||
addon->Draw();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
|
||||
}
|
||||
|
||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||
{
|
||||
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.TimeDeltaInternal = delta;
|
||||
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
|
||||
|
||||
try
|
||||
{
|
||||
addon->Update(delta);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
|
||||
}
|
||||
|
||||
private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.AtkValueCount = valueCount;
|
||||
arg.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
|
||||
valueCount = arg.AtkValueCount;
|
||||
values = (AtkValue*)arg.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||
{
|
||||
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.NumberArrayData = (nint)numberArrayData;
|
||||
arg.StringArrayData = (nint)stringArrayData;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
|
||||
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
|
||||
stringArrayData = (StringArrayData**)arg.StringArrayData;
|
||||
|
||||
try
|
||||
{
|
||||
addon->OnRequestedUpdate(numberArrayData, stringArrayData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
|
||||
Log.Debug($"Initializing {addonName}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +228,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
|
||||
|
||||
private readonly List<AddonLifecycleEventListener> eventListeners = new();
|
||||
private readonly List<AddonLifecycleEventListener> eventListeners = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
|
|
@ -458,10 +299,14 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
|
|||
this.eventListeners.RemoveAll(entry =>
|
||||
{
|
||||
if (entry.FunctionDelegate != handler) return false;
|
||||
|
||||
|
||||
this.addonLifecycleService.UnregisterListener(entry);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
|
||||
=> (nint)AddonLifecycle.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// AddonLifecycleService memory address resolver.
|
||||
/// </summary>
|
||||
internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the addon setup hook invoked by the AtkUnitManager.
|
||||
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
|
||||
/// This is called for a majority of all addon OnSetup's.
|
||||
/// </summary>
|
||||
public nint AddonSetup { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
|
||||
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
|
||||
/// This seems to be called rarely for specific addons.
|
||||
/// </summary>
|
||||
public nint AddonSetup2 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
|
||||
/// </summary>
|
||||
public nint AddonFinalize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon draw hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonDraw { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon update hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonOnRequestedUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan for and setup any configured address pointers.
|
||||
/// </summary>
|
||||
/// <param name="sig">The signature scanner to facilitate setup.</param>
|
||||
protected override void Setup64Bit(ISigScanner sig)
|
||||
{
|
||||
this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB");
|
||||
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
|
||||
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
|
||||
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
|
||||
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
|
||||
}
|
||||
}
|
||||
|
|
@ -25,19 +25,19 @@ internal class AddonLifecycleEventListener
|
|||
/// string.Empty if it wants to be called for any addon.
|
||||
/// </summary>
|
||||
public string AddonName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this event has been unregistered.
|
||||
/// </summary>
|
||||
public bool Removed { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event type this listener is looking for.
|
||||
/// </summary>
|
||||
public AddonEvent EventType { get; init; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delegate this listener invokes.
|
||||
/// </summary>
|
||||
public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the listener is requested to be cleared.
|
||||
/// </summary>
|
||||
internal bool IsRequestedToClear { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
|
||||
/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
|
||||
/// </summary>
|
||||
internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="service">AddonLifecycle service instance.</param>
|
||||
/// <param name="addonName">Initial Addon Requesting this listener.</param>
|
||||
/// <param name="receiveEventAddress">Address of Addon's ReceiveEvent function.</param>
|
||||
internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
|
||||
{
|
||||
this.AddonLifecycle = service;
|
||||
this.AddonNames = [addonName];
|
||||
this.FunctionAddress = receiveEventAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of addons that use this receive event hook.
|
||||
/// </summary>
|
||||
public List<string> AddonNames { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
|
||||
/// </summary>
|
||||
public nint FunctionAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the contained hook for these addons.
|
||||
/// </summary>
|
||||
public Hook<AtkUnitBase.Delegates.ReceiveEvent>? Hook { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Reference to AddonLifecycle service instance.
|
||||
/// </summary>
|
||||
private AddonLifecycle AddonLifecycle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Try to hook and enable this receive event handler.
|
||||
/// </summary>
|
||||
public void TryEnable()
|
||||
{
|
||||
this.Hook ??= Hook<AtkUnitBase.Delegates.ReceiveEvent>.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
|
||||
this.Hook?.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable the hook for this receive event handler.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
this.Hook?.Disable();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Hook?.Dispose();
|
||||
}
|
||||
|
||||
private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
// Check that we didn't get here through a call to another addons handler.
|
||||
var addonName = addon->NameString;
|
||||
if (!this.AddonNames.Contains(addonName))
|
||||
{
|
||||
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
|
||||
return;
|
||||
}
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.AtkEventType = (byte)eventType;
|
||||
arg.EventParam = eventParam;
|
||||
arg.AtkEvent = (IntPtr)atkEvent;
|
||||
arg.Data = (nint)atkEventData;
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
|
||||
eventType = (AtkEventType)arg.AtkEventType;
|
||||
eventParam = arg.EventParam;
|
||||
atkEvent = (AtkEvent*)arg.AtkEvent;
|
||||
atkEventData = (AtkEventData*)arg.Data;
|
||||
|
||||
try
|
||||
{
|
||||
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
using Reloaded.Hooks.Definitions;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
|
||||
internal class AddonSetupHook<T> : IDisposable where T : Delegate
|
||||
{
|
||||
private readonly Reloaded.Hooks.AsmHook asmHook;
|
||||
|
||||
private T? detour;
|
||||
private bool activated;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonSetupHook{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the instruction to replace.</param>
|
||||
/// <param name="detour">Delegate to invoke.</param>
|
||||
internal AddonSetupHook(nint address, T detour)
|
||||
{
|
||||
this.detour = detour;
|
||||
|
||||
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
|
||||
var code = new[]
|
||||
{
|
||||
"use64",
|
||||
$"mov r9, 0x{detourPtr:X8}",
|
||||
};
|
||||
|
||||
var opt = new AsmHookOptions
|
||||
{
|
||||
PreferRelativeJump = true,
|
||||
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
|
||||
MaxOpcodeSize = 5,
|
||||
};
|
||||
|
||||
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the hook is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled => this.asmHook.IsEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Starts intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
if (!this.activated)
|
||||
{
|
||||
this.activated = true;
|
||||
this.asmHook.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.asmHook.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a hook from the current process.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
this.detour = null;
|
||||
}
|
||||
}
|
||||
679
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
Normal file
679
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
Normal file
|
|
@ -0,0 +1,679 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a class that holds references to an addons original and modified virtual table entries.
|
||||
/// </summary>
|
||||
internal unsafe class AddonVirtualTable : IDisposable
|
||||
{
|
||||
// This need to be at minimum the largest virtual table size of all addons
|
||||
// Copying extra entries is not problematic, and is considered safe.
|
||||
private const int VirtualTableEntryCount = 200;
|
||||
|
||||
private const bool EnableLogging = false;
|
||||
|
||||
private static readonly ModuleLog Log = new("LifecycleVT");
|
||||
|
||||
private readonly AddonLifecycle lifecycleService;
|
||||
|
||||
// Each addon gets its own set of args that are used to mutate the original call when used in pre-calls
|
||||
private readonly AddonSetupArgs setupArgs = new();
|
||||
private readonly AddonArgs finalizeArgs = new();
|
||||
private readonly AddonArgs drawArgs = new();
|
||||
private readonly AddonArgs updateArgs = new();
|
||||
private readonly AddonRefreshArgs refreshArgs = new();
|
||||
private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new();
|
||||
private readonly AddonReceiveEventArgs receiveEventArgs = new();
|
||||
private readonly AddonArgs openArgs = new();
|
||||
private readonly AddonCloseArgs closeArgs = new();
|
||||
private readonly AddonShowArgs showArgs = new();
|
||||
private readonly AddonHideArgs hideArgs = new();
|
||||
private readonly AddonArgs onMoveArgs = new();
|
||||
private readonly AddonArgs onMouseOverArgs = new();
|
||||
private readonly AddonArgs onMouseOutArgs = new();
|
||||
private readonly AddonArgs focusArgs = new();
|
||||
private readonly AddonFocusChangedArgs focusChangedArgs = new();
|
||||
|
||||
private readonly AtkUnitBase* atkUnitBase;
|
||||
|
||||
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
|
||||
// the CLR needs to know they are in use, or it will invalidate them causing random crashing.
|
||||
private readonly AtkUnitBase.Delegates.Dtor destructorFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction;
|
||||
private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction;
|
||||
private readonly AtkUnitBase.Delegates.Draw drawFunction;
|
||||
private readonly AtkUnitBase.Delegates.Update updateFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction;
|
||||
private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction;
|
||||
private readonly AtkUnitBase.Delegates.Open openFunction;
|
||||
private readonly AtkUnitBase.Delegates.Close closeFunction;
|
||||
private readonly AtkUnitBase.Delegates.Show showFunction;
|
||||
private readonly AtkUnitBase.Delegates.Hide hideFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnMove onMoveFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
|
||||
private readonly AtkUnitBase.Delegates.Focus focusFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addon">AtkUnitBase* for the addon to replace the table of.</param>
|
||||
/// <param name="lifecycleService">Reference to AddonLifecycle service to callback and invoke listeners.</param>
|
||||
internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
|
||||
{
|
||||
this.atkUnitBase = addon;
|
||||
this.lifecycleService = lifecycleService;
|
||||
|
||||
// Save original virtual table
|
||||
this.OriginalVirtualTable = addon->VirtualTable;
|
||||
|
||||
// Create copy of original table
|
||||
// Note this will copy any derived/overriden functions that this specific addon has.
|
||||
// Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
|
||||
this.ModifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
|
||||
NativeMemory.Copy(addon->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
|
||||
// Overwrite the addons existing virtual table with our own
|
||||
addon->VirtualTable = this.ModifiedVirtualTable;
|
||||
|
||||
// Pin each of our listener functions
|
||||
this.destructorFunction = this.OnAddonDestructor;
|
||||
this.onSetupFunction = this.OnAddonSetup;
|
||||
this.finalizerFunction = this.OnAddonFinalize;
|
||||
this.drawFunction = this.OnAddonDraw;
|
||||
this.updateFunction = this.OnAddonUpdate;
|
||||
this.onRefreshFunction = this.OnAddonRefresh;
|
||||
this.onRequestedUpdateFunction = this.OnRequestedUpdate;
|
||||
this.onReceiveEventFunction = this.OnAddonReceiveEvent;
|
||||
this.openFunction = this.OnAddonOpen;
|
||||
this.closeFunction = this.OnAddonClose;
|
||||
this.showFunction = this.OnAddonShow;
|
||||
this.hideFunction = this.OnAddonHide;
|
||||
this.onMoveFunction = this.OnAddonMove;
|
||||
this.onMouseOverFunction = this.OnAddonMouseOver;
|
||||
this.onMouseOutFunction = this.OnAddonMouseOut;
|
||||
this.focusFunction = this.OnAddonFocus;
|
||||
this.onFocusChangeFunction = this.OnAddonFocusChange;
|
||||
|
||||
// Overwrite specific virtual table entries
|
||||
this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
|
||||
this.ModifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
|
||||
this.ModifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
|
||||
this.ModifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
|
||||
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
|
||||
this.ModifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
|
||||
this.ModifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
|
||||
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
|
||||
this.ModifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
|
||||
this.ModifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
|
||||
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
|
||||
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
|
||||
this.ModifiedVirtualTable->OnMove = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
|
||||
this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
|
||||
this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
|
||||
this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
|
||||
this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged<AtkUnitBase*, bool, void>)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original virtual table address for this addon.
|
||||
/// </summary>
|
||||
internal AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the modified virtual address for this addon.
|
||||
/// </summary>
|
||||
internal AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// Ensure restoration is done atomically.
|
||||
Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.OriginalVirtualTable);
|
||||
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
}
|
||||
|
||||
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
|
||||
{
|
||||
AtkEventListener* result = null;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
try
|
||||
{
|
||||
result = this.OriginalVirtualTable->Dtor(thisPtr, freeFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Dtor. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
if ((freeFlags & 1) == 1)
|
||||
{
|
||||
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
AddonLifecycle.AllocatedTables.Remove(this);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDestructor.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.setupArgs.Addon = addon;
|
||||
this.setupArgs.AtkValueCount = valueCount;
|
||||
this.setupArgs.AtkValues = (nint)values;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs);
|
||||
|
||||
valueCount = this.setupArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.setupArgs.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnSetup(addon, valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnSetup. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonSetup.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonFinalize(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.finalizeArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Finalizer(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Finalizer. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFinalize.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonDraw(AtkUnitBase* addon)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.drawArgs.Addon = addon;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Draw(addon);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Draw. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDraw.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.updateArgs.Addon = addon;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs);
|
||||
|
||||
// Note: Do not pass or allow manipulation of delta.
|
||||
// It's realistically not something that should be needed.
|
||||
// And even if someone does, they are encouraged to hook Update themselves.
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Update(addon, delta);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonUpdate.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.refreshArgs.Addon = addon;
|
||||
this.refreshArgs.AtkValueCount = valueCount;
|
||||
this.refreshArgs.AtkValues = (nint)values;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs);
|
||||
|
||||
valueCount = this.refreshArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.refreshArgs.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.OriginalVirtualTable->OnRefresh(addon, valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnRefresh. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonRefresh.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.requestedUpdateArgs.Addon = addon;
|
||||
this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
|
||||
this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs);
|
||||
|
||||
numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData;
|
||||
stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnRequestedUpdate.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.receiveEventArgs.Addon = (nint)addon;
|
||||
this.receiveEventArgs.AtkEventType = (byte)eventType;
|
||||
this.receiveEventArgs.EventParam = eventParam;
|
||||
this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent;
|
||||
this.receiveEventArgs.AtkEventData = (nint)atkEventData;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs);
|
||||
|
||||
eventType = (AtkEventType)this.receiveEventArgs.AtkEventType;
|
||||
eventParam = this.receiveEventArgs.EventParam;
|
||||
atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent;
|
||||
atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonReceiveEvent.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.openArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs);
|
||||
|
||||
try
|
||||
{
|
||||
result = this.OriginalVirtualTable->Open(thisPtr, depthLayer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Open. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonOpen.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.closeArgs.Addon = thisPtr;
|
||||
this.closeArgs.FireCallback = fireCallback;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs);
|
||||
|
||||
fireCallback = this.closeArgs.FireCallback;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.OriginalVirtualTable->Close(thisPtr, fireCallback);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Close. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonClose.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.showArgs.Addon = thisPtr;
|
||||
this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect;
|
||||
this.showArgs.UnsetShowHideFlags = unsetShowHideFlags;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs);
|
||||
|
||||
silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect;
|
||||
unsetShowHideFlags = this.showArgs.UnsetShowHideFlags;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonShow.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.hideArgs.Addon = thisPtr;
|
||||
this.hideArgs.UnknownBool = unkBool;
|
||||
this.hideArgs.CallHideCallback = callHideCallback;
|
||||
this.hideArgs.SetShowHideFlags = setShowHideFlags;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs);
|
||||
|
||||
unkBool = this.hideArgs.UnknownBool;
|
||||
callHideCallback = this.hideArgs.CallHideCallback;
|
||||
setShowHideFlags = this.hideArgs.SetShowHideFlags;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonHide.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonMove(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.onMoveArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnMove(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnMove. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMove.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonMouseOver(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.onMouseOverArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnMouseOver(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnMouseOver. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOver.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonMouseOut(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.onMouseOutArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnMouseOut(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnMouseOut. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOut.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonFocus(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.focusArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Focus(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Focus. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocus.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonFocusChange(AtkUnitBase* thisPtr, bool isFocused)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.focusChangedArgs.Addon = thisPtr;
|
||||
this.focusChangedArgs.ShouldFocus = isFocused;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocusChanged, this.focusChangedArgs);
|
||||
|
||||
isFocused = this.focusChangedArgs.ShouldFocus;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnFocusChange(thisPtr, isFocused);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnFocusChanged. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocusChanged, this.focusChangedArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocusChange.");
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
|
||||
{
|
||||
if (loggingEnabled)
|
||||
{
|
||||
// Manually disable the really spammy log events, you can comment this out if you need to debug them.
|
||||
if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
|
||||
return;
|
||||
|
||||
Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs
Normal file
39
Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using Dalamud.Game.NativeWrapper;
|
||||
|
||||
namespace Dalamud.Game.Agent.AgentArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for AgentLifecycle AgentArgTypes.
|
||||
/// </summary>
|
||||
public unsafe class AgentArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentArgs"/> class.
|
||||
/// </summary>
|
||||
internal AgentArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pointer to the Agents AgentInterface*.
|
||||
/// </summary>
|
||||
public AgentInterfacePtr Agent { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent id.
|
||||
/// </summary>
|
||||
public AgentId AgentId { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of these args.
|
||||
/// </summary>
|
||||
public virtual AgentArgsType Type => AgentArgsType.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the typed pointer to the Agents AgentInterface*.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">AgentInterface.</typeparam>
|
||||
/// <returns>Typed pointer to contained Agents AgentInterface.</returns>
|
||||
public T* GetAgentPointer<T>() where T : unmanaged
|
||||
=> (T*)this.Agent.Address;
|
||||
}
|
||||
22
Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs
Normal file
22
Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Dalamud.Game.Agent.AgentArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Agent argument data for game events.
|
||||
/// </summary>
|
||||
public class AgentClassJobChangeArgs : AgentArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentClassJobChangeArgs"/> class.
|
||||
/// </summary>
|
||||
internal AgentClassJobChangeArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentArgsType Type => AgentArgsType.ClassJobChange;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating what the new ClassJob is.
|
||||
/// </summary>
|
||||
public byte ClassJobId { get; set; }
|
||||
}
|
||||
22
Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs
Normal file
22
Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Dalamud.Game.Agent.AgentArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Agent argument data for game events.
|
||||
/// </summary>
|
||||
public class AgentGameEventArgs : AgentArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentGameEventArgs"/> class.
|
||||
/// </summary>
|
||||
internal AgentGameEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentArgsType Type => AgentArgsType.GameEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value representing which gameEvent was triggered.
|
||||
/// </summary>
|
||||
public int GameEvent { get; set; }
|
||||
}
|
||||
27
Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs
Normal file
27
Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
namespace Dalamud.Game.Agent.AgentArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Agent argument data for game events.
|
||||
/// </summary>
|
||||
public class AgentLevelChangeArgs : AgentArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentLevelChangeArgs"/> class.
|
||||
/// </summary>
|
||||
internal AgentLevelChangeArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentArgsType Type => AgentArgsType.LevelChange;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating which ClassJob was switched to.
|
||||
/// </summary>
|
||||
public byte ClassJobId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating what the new level is.
|
||||
/// </summary>
|
||||
public ushort Level { get; set; }
|
||||
}
|
||||
37
Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs
Normal file
37
Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
namespace Dalamud.Game.Agent.AgentArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Agent argument data for ReceiveEvent events.
|
||||
/// </summary>
|
||||
public class AgentReceiveEventArgs : AgentArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentReceiveEventArgs"/> class.
|
||||
/// </summary>
|
||||
internal AgentReceiveEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentArgsType Type => AgentArgsType.ReceiveEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AtkValue return value for this event message.
|
||||
/// </summary>
|
||||
public nint ReturnValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AtkValue array for this event message.
|
||||
/// </summary>
|
||||
public nint AtkValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AtkValue count for this event message.
|
||||
/// </summary>
|
||||
public uint ValueCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event kind for this event message.
|
||||
/// </summary>
|
||||
public ulong EventKind { get; set; }
|
||||
}
|
||||
32
Dalamud/Game/Agent/AgentArgsType.cs
Normal file
32
Dalamud/Game/Agent/AgentArgsType.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
namespace Dalamud.Game.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// Enumeration for available AgentLifecycle arg data.
|
||||
/// </summary>
|
||||
public enum AgentArgsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic arg type that contains no meaningful data.
|
||||
/// </summary>
|
||||
Generic,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for ReceiveEvent.
|
||||
/// </summary>
|
||||
ReceiveEvent,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for GameEvent.
|
||||
/// </summary>
|
||||
GameEvent,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for LevelChange.
|
||||
/// </summary>
|
||||
LevelChange,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for ClassJobChange.
|
||||
/// </summary>
|
||||
ClassJobChange,
|
||||
}
|
||||
87
Dalamud/Game/Agent/AgentEvent.cs
Normal file
87
Dalamud/Game/Agent/AgentEvent.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
namespace Dalamud.Game.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// Enumeration for available AgentLifecycle events.
|
||||
/// </summary>
|
||||
public enum AgentEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its Receive Event Function.
|
||||
/// </summary>
|
||||
PreReceiveEvent,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its Receive Event Function.
|
||||
/// </summary>
|
||||
PostReceiveEvent,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its Filtered Receive Event Function.
|
||||
/// </summary>
|
||||
PreReceiveEventWithResult,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its Filtered Receive Event Function.
|
||||
/// </summary>
|
||||
PostReceiveEventWithResult,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its Show Function.
|
||||
/// </summary>
|
||||
PreShow,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its Show Function.
|
||||
/// </summary>
|
||||
PostShow,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its Hide Function.
|
||||
/// </summary>
|
||||
PreHide,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its Hide Function.
|
||||
/// </summary>
|
||||
PostHide,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its Update Function.
|
||||
/// </summary>
|
||||
PreUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its Update Function.
|
||||
/// </summary>
|
||||
PostUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its Game Event Function.
|
||||
/// </summary>
|
||||
PreGameEvent,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its Game Event Function.
|
||||
/// </summary>
|
||||
PostGameEvent,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its Game Event Function.
|
||||
/// </summary>
|
||||
PreLevelChange,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its Level Change Function.
|
||||
/// </summary>
|
||||
PostLevelChange,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before the agent processes its ClassJob Change Function.
|
||||
/// </summary>
|
||||
PreClassJobChange,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after the agent has processed its ClassJob Change Function.
|
||||
/// </summary>
|
||||
PostClassJobChange,
|
||||
}
|
||||
344
Dalamud/Game/Agent/AgentLifecycle.cs
Normal file
344
Dalamud/Game/Agent/AgentLifecycle.cs
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Game.Agent.AgentArgTypes;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
namespace Dalamud.Game.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides events for in-game agent lifecycles.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class AgentLifecycle : IInternalDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a list of all allocated agent virtual tables.
|
||||
/// </summary>
|
||||
public static readonly List<AgentVirtualTable> AllocatedTables = [];
|
||||
|
||||
private static readonly ModuleLog Log = new("AgentLifecycle");
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
private Hook<AgentModule.Delegates.Ctor>? onInitializeAgentsHook;
|
||||
private bool isInvokingListeners;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AgentLifecycle()
|
||||
{
|
||||
var agentModuleInstance = AgentModule.Instance();
|
||||
|
||||
// Hook is only used to determine appropriate timing for replacing Agent Virtual Tables
|
||||
// If the agent module is already initialized, then we can replace the tables safely.
|
||||
if (agentModuleInstance is null)
|
||||
{
|
||||
this.onInitializeAgentsHook = Hook<AgentModule.Delegates.Ctor>.FromAddress((nint)AgentModule.MemberFunctionPointers.Ctor, this.OnAgentModuleInitialize);
|
||||
this.onInitializeAgentsHook.Enable();
|
||||
}
|
||||
else
|
||||
{
|
||||
// For safety because this might be injected async, we will make sure we are on the main thread first.
|
||||
this.framework.RunOnFrameworkThread(() => this.ReplaceVirtualTables(agentModuleInstance));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all AgentLifecycle Event Listeners.
|
||||
/// </summary> <br/>
|
||||
/// Mapping is: EventType -> ListenerList
|
||||
internal Dictionary<AgentEvent, Dictionary<AgentId, HashSet<AgentLifecycleEventListener>>> EventListeners { get; } = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.onInitializeAgentsHook?.Dispose();
|
||||
this.onInitializeAgentsHook = null;
|
||||
|
||||
AllocatedTables.ForEach(entry => entry.Dispose());
|
||||
AllocatedTables.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a virtual table address to the original virtual table address.
|
||||
/// </summary>
|
||||
/// <param name="tableAddress">The modified address to resolve.</param>
|
||||
/// <returns>The original address.</returns>
|
||||
internal static AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress)
|
||||
{
|
||||
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
|
||||
if (matchedTable == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return matchedTable.OriginalVirtualTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a listener for the target event and agent.
|
||||
/// </summary>
|
||||
/// <param name="listener">The listener to register.</param>
|
||||
internal void RegisterListener(AgentLifecycleEventListener listener)
|
||||
{
|
||||
if (this.isInvokingListeners)
|
||||
{
|
||||
this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the listener from events.
|
||||
/// </summary>
|
||||
/// <param name="listener">The listener to unregister.</param>
|
||||
internal void UnregisterListener(AgentLifecycleEventListener listener)
|
||||
{
|
||||
listener.IsRequestedToClear = true;
|
||||
|
||||
if (this.isInvokingListeners)
|
||||
{
|
||||
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke listeners for the specified event type.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Event Type.</param>
|
||||
/// <param name="args">AgentARgs.</param>
|
||||
/// <param name="blame">What to blame on errors.</param>
|
||||
internal void InvokeListenersSafely(AgentEvent eventType, AgentArgs args, [CallerMemberName] string blame = "")
|
||||
{
|
||||
this.isInvokingListeners = true;
|
||||
|
||||
// Early return if we don't have any listeners of this type
|
||||
if (!this.EventListeners.TryGetValue(eventType, out var agentListeners)) return;
|
||||
|
||||
// Handle listeners for this event type that don't care which agent is triggering it
|
||||
if (agentListeners.TryGetValue((AgentId)uint.MaxValue, out var globalListeners))
|
||||
{
|
||||
foreach (var listener in globalListeners)
|
||||
{
|
||||
if (listener.IsRequestedToClear) continue;
|
||||
|
||||
try
|
||||
{
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global agent event listener.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle listeners that are listening for this agent and event type specifically
|
||||
if (agentListeners.TryGetValue(args.AgentId, out var agentListener))
|
||||
{
|
||||
foreach (var listener in agentListener)
|
||||
{
|
||||
if (listener.IsRequestedToClear) continue;
|
||||
|
||||
try
|
||||
{
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {args.AgentId}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isInvokingListeners = false;
|
||||
}
|
||||
|
||||
private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule)
|
||||
{
|
||||
this.onInitializeAgentsHook!.Original(thisPtr, uiModule);
|
||||
|
||||
try
|
||||
{
|
||||
this.ReplaceVirtualTables(thisPtr);
|
||||
|
||||
// We don't need this hook anymore, it did its job!
|
||||
this.onInitializeAgentsHook!.Dispose();
|
||||
this.onInitializeAgentsHook = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in AgentLifecycle during AgentModule Ctor.");
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterListenerMethod(AgentLifecycleEventListener listener)
|
||||
{
|
||||
if (!this.EventListeners.ContainsKey(listener.EventType))
|
||||
{
|
||||
if (!this.EventListeners.TryAdd(listener.EventType, []))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type
|
||||
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId))
|
||||
{
|
||||
if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, []))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.EventListeners[listener.EventType][listener.AgentId].Add(listener);
|
||||
}
|
||||
|
||||
private void UnregisterListenerMethod(AgentLifecycleEventListener listener)
|
||||
{
|
||||
if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners))
|
||||
{
|
||||
if (agentListeners.TryGetValue(listener.AgentId, out var agentListener))
|
||||
{
|
||||
agentListener.Remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReplaceVirtualTables(AgentModule* agentModule)
|
||||
{
|
||||
foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length))
|
||||
{
|
||||
try
|
||||
{
|
||||
var agentPointer = agentModule->Agents.GetPointer((int)index);
|
||||
|
||||
if (agentPointer is null)
|
||||
{
|
||||
Log.Warning("Null Agent Found?");
|
||||
continue;
|
||||
}
|
||||
|
||||
// AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
|
||||
AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, (AgentId)index, this));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in AgentLifecycle during ReplaceVirtualTables.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a AgentLifecycle service.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IAgentLifecycle>]
|
||||
#pragma warning restore SA1015
|
||||
internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLifecycle
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AgentLifecycle agentLifecycleService = Service<AgentLifecycle>.Get();
|
||||
|
||||
private readonly List<AgentLifecycleEventListener> eventListeners = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
foreach (var listener in this.eventListeners)
|
||||
{
|
||||
this.agentLifecycleService.UnregisterListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate handler)
|
||||
{
|
||||
foreach (var agentId in agentIds)
|
||||
{
|
||||
this.RegisterListener(eventType, agentId, handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate handler)
|
||||
{
|
||||
var listener = new AgentLifecycleEventListener(eventType, agentId, handler);
|
||||
this.eventListeners.Add(listener);
|
||||
this.agentLifecycleService.RegisterListener(listener);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler)
|
||||
{
|
||||
this.RegisterListener(eventType, (AgentId)uint.MaxValue, handler);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate? handler = null)
|
||||
{
|
||||
foreach (var agentId in agentIds)
|
||||
{
|
||||
this.UnregisterListener(eventType, agentId, handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate? handler = null)
|
||||
{
|
||||
this.eventListeners.RemoveAll(entry =>
|
||||
{
|
||||
if (entry.EventType != eventType) return false;
|
||||
if (entry.AgentId != agentId) return false;
|
||||
if (handler is not null && entry.FunctionDelegate != handler) return false;
|
||||
|
||||
this.agentLifecycleService.UnregisterListener(entry);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null)
|
||||
{
|
||||
this.UnregisterListener(eventType, (AgentId)uint.MaxValue, handler);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(params IAgentLifecycle.AgentEventDelegate[] handlers)
|
||||
{
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
this.eventListeners.RemoveAll(entry =>
|
||||
{
|
||||
if (entry.FunctionDelegate != handler) return false;
|
||||
|
||||
this.agentLifecycleService.UnregisterListener(entry);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
|
||||
=> (nint)AgentLifecycle.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress);
|
||||
}
|
||||
43
Dalamud/Game/Agent/AgentLifecycleEventListener.cs
Normal file
43
Dalamud/Game/Agent/AgentLifecycleEventListener.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// This class is a helper for tracking and invoking listener delegates.
|
||||
/// </summary>
|
||||
public class AgentLifecycleEventListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentLifecycleEventListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Event type to listen for.</param>
|
||||
/// <param name="agentId">Agent id to listen for.</param>
|
||||
/// <param name="functionDelegate">Delegate to invoke.</param>
|
||||
internal AgentLifecycleEventListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate functionDelegate)
|
||||
{
|
||||
this.EventType = eventType;
|
||||
this.AgentId = agentId;
|
||||
this.FunctionDelegate = functionDelegate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agentId of the agent this listener is looking for.
|
||||
/// uint.MaxValue if it wants to be called for any agent.
|
||||
/// </summary>
|
||||
public AgentId AgentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event type this listener is looking for.
|
||||
/// </summary>
|
||||
public AgentEvent EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delegate this listener invokes.
|
||||
/// </summary>
|
||||
public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the listener is requested to be cleared.
|
||||
/// </summary>
|
||||
internal bool IsRequestedToClear { get; set; }
|
||||
}
|
||||
391
Dalamud/Game/Agent/AgentVirtualTable.cs
Normal file
391
Dalamud/Game/Agent/AgentVirtualTable.cs
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Game.Agent.AgentArgTypes;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a class that holds references to an agents original and modified virtual table entries.
|
||||
/// </summary>
|
||||
internal unsafe class AgentVirtualTable : IDisposable
|
||||
{
|
||||
// This need to be at minimum the largest virtual table size of all agents
|
||||
// Copying extra entries is not problematic, and is considered safe.
|
||||
private const int VirtualTableEntryCount = 60;
|
||||
|
||||
private const bool EnableLogging = false;
|
||||
|
||||
private static readonly ModuleLog Log = new("AgentVT");
|
||||
|
||||
private readonly AgentLifecycle lifecycleService;
|
||||
|
||||
private readonly AgentId agentId;
|
||||
|
||||
// Each agent gets its own set of args that are used to mutate the original call when used in pre-calls
|
||||
private readonly AgentReceiveEventArgs receiveEventArgs = new();
|
||||
private readonly AgentReceiveEventArgs filteredReceiveEventArgs = new();
|
||||
private readonly AgentArgs showArgs = new();
|
||||
private readonly AgentArgs hideArgs = new();
|
||||
private readonly AgentArgs updateArgs = new();
|
||||
private readonly AgentGameEventArgs gameEventArgs = new();
|
||||
private readonly AgentLevelChangeArgs levelChangeArgs = new();
|
||||
private readonly AgentClassJobChangeArgs classJobChangeArgs = new();
|
||||
|
||||
private readonly AgentInterface* agentInterface;
|
||||
|
||||
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
|
||||
// the CLR needs to know they are in use, or it will invalidate them causing random crashing.
|
||||
private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction;
|
||||
private readonly AgentInterface.Delegates.ReceiveEventWithResult receiveEventWithResultFunction;
|
||||
private readonly AgentInterface.Delegates.Show showFunction;
|
||||
private readonly AgentInterface.Delegates.Hide hideFunction;
|
||||
private readonly AgentInterface.Delegates.Update updateFunction;
|
||||
private readonly AgentInterface.Delegates.OnGameEvent gameEventFunction;
|
||||
private readonly AgentInterface.Delegates.OnLevelChange levelChangeFunction;
|
||||
private readonly AgentInterface.Delegates.OnClassJobChange classJobChangeFunction;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentVirtualTable"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agent">AgentInterface* for the agent to replace the table of.</param>
|
||||
/// <param name="agentId">Agent ID.</param>
|
||||
/// <param name="lifecycleService">Reference to AgentLifecycle service to callback and invoke listeners.</param>
|
||||
internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService)
|
||||
{
|
||||
this.agentInterface = agent;
|
||||
this.agentId = agentId;
|
||||
this.lifecycleService = lifecycleService;
|
||||
|
||||
// Save original virtual table
|
||||
this.OriginalVirtualTable = agent->VirtualTable;
|
||||
|
||||
// Create copy of original table
|
||||
// Note this will copy any derived/overriden functions that this specific agent has.
|
||||
// Note: currently there are 16 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
|
||||
this.ModifiedVirtualTable = (AgentInterface.AgentInterfaceVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
|
||||
NativeMemory.Copy(agent->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
|
||||
// Overwrite the agents existing virtual table with our own
|
||||
agent->VirtualTable = this.ModifiedVirtualTable;
|
||||
|
||||
// Pin each of our listener functions
|
||||
this.receiveEventFunction = this.OnAgentReceiveEvent;
|
||||
this.receiveEventWithResultFunction = this.OnAgentReceiveEventWithResult;
|
||||
this.showFunction = this.OnAgentShow;
|
||||
this.hideFunction = this.OnAgentHide;
|
||||
this.updateFunction = this.OnAgentUpdate;
|
||||
this.gameEventFunction = this.OnAgentGameEvent;
|
||||
this.levelChangeFunction = this.OnAgentLevelChange;
|
||||
this.classJobChangeFunction = this.OnClassJobChange;
|
||||
|
||||
// Overwrite specific virtual table entries
|
||||
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction);
|
||||
this.ModifiedVirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventWithResultFunction);
|
||||
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
|
||||
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
|
||||
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AgentInterface*, uint, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
|
||||
this.ModifiedVirtualTable->OnGameEvent = (delegate* unmanaged<AgentInterface*, AgentInterface.GameEvent, void>)Marshal.GetFunctionPointerForDelegate(this.gameEventFunction);
|
||||
this.ModifiedVirtualTable->OnLevelChange = (delegate* unmanaged<AgentInterface*, byte, ushort, void>)Marshal.GetFunctionPointerForDelegate(this.levelChangeFunction);
|
||||
this.ModifiedVirtualTable->OnClassJobChange = (delegate* unmanaged<AgentInterface*, byte, void>)Marshal.GetFunctionPointerForDelegate(this.classJobChangeFunction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original virtual table address for this agent.
|
||||
/// </summary>
|
||||
internal AgentInterface.AgentInterfaceVirtualTable* OriginalVirtualTable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the modified virtual address for this agent.
|
||||
/// </summary>
|
||||
internal AgentInterface.AgentInterfaceVirtualTable* ModifiedVirtualTable { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// Ensure restoration is done atomically.
|
||||
Interlocked.Exchange(ref *(nint*)&this.agentInterface->VirtualTable, (nint)this.OriginalVirtualTable);
|
||||
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
}
|
||||
|
||||
private AtkValue* OnAgentReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
|
||||
{
|
||||
AtkValue* result = null;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.receiveEventArgs.Agent = thisPtr;
|
||||
this.receiveEventArgs.AgentId = this.agentId;
|
||||
this.receiveEventArgs.ReturnValue = (nint)returnValue;
|
||||
this.receiveEventArgs.AtkValues = (nint)values;
|
||||
this.receiveEventArgs.ValueCount = valueCount;
|
||||
this.receiveEventArgs.EventKind = eventKind;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEvent, this.receiveEventArgs);
|
||||
|
||||
returnValue = (AtkValue*)this.receiveEventArgs.ReturnValue;
|
||||
values = (AtkValue*)this.receiveEventArgs.AtkValues;
|
||||
valueCount = this.receiveEventArgs.ValueCount;
|
||||
eventKind = this.receiveEventArgs.EventKind;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.OriginalVirtualTable->ReceiveEvent(thisPtr, returnValue, values, valueCount, eventKind);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Agent ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEvent, this.receiveEventArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEvent.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private AtkValue* OnAgentReceiveEventWithResult(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
|
||||
{
|
||||
AtkValue* result = null;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.filteredReceiveEventArgs.Agent = thisPtr;
|
||||
this.filteredReceiveEventArgs.AgentId = this.agentId;
|
||||
this.filteredReceiveEventArgs.ReturnValue = (nint)returnValue;
|
||||
this.filteredReceiveEventArgs.AtkValues = (nint)values;
|
||||
this.filteredReceiveEventArgs.ValueCount = valueCount;
|
||||
this.filteredReceiveEventArgs.EventKind = eventKind;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEventWithResult, this.filteredReceiveEventArgs);
|
||||
|
||||
returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue;
|
||||
values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues;
|
||||
valueCount = this.filteredReceiveEventArgs.ValueCount;
|
||||
eventKind = this.filteredReceiveEventArgs.EventKind;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.OriginalVirtualTable->ReceiveEventWithResult(thisPtr, returnValue, values, valueCount, eventKind);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEventWithResult, this.filteredReceiveEventArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEventWithResult.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnAgentShow(AgentInterface* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.showArgs.Agent = thisPtr;
|
||||
this.showArgs.AgentId = this.agentId;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreShow, this.showArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Show(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostShow, this.showArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentShow.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAgentHide(AgentInterface* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.hideArgs.Agent = thisPtr;
|
||||
this.hideArgs.AgentId = this.agentId;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreHide, this.hideArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Hide(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostHide, this.hideArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentHide.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAgentUpdate(AgentInterface* thisPtr, uint frameCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.updateArgs.Agent = thisPtr;
|
||||
this.updateArgs.AgentId = this.agentId;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreUpdate, this.updateArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->Update(thisPtr, frameCount);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostUpdate, this.updateArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentUpdate.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAgentGameEvent(AgentInterface* thisPtr, AgentInterface.GameEvent gameEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.gameEventArgs.Agent = thisPtr;
|
||||
this.gameEventArgs.AgentId = this.agentId;
|
||||
this.gameEventArgs.GameEvent = (int)gameEvent;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreGameEvent, this.gameEventArgs);
|
||||
|
||||
gameEvent = (AgentInterface.GameEvent)this.gameEventArgs.GameEvent;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnGameEvent(thisPtr, gameEvent);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnGameEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostGameEvent, this.gameEventArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentGameEvent.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAgentLevelChange(AgentInterface* thisPtr, byte classJobId, ushort level)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.levelChangeArgs.Agent = thisPtr;
|
||||
this.levelChangeArgs.AgentId = this.agentId;
|
||||
this.levelChangeArgs.ClassJobId = classJobId;
|
||||
this.levelChangeArgs.Level = level;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreLevelChange, this.levelChangeArgs);
|
||||
|
||||
classJobId = this.levelChangeArgs.ClassJobId;
|
||||
level = this.levelChangeArgs.Level;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnLevelChange(thisPtr, classJobId, level);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnLevelChange. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostLevelChange, this.levelChangeArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentLevelChange.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClassJobChange(AgentInterface* thisPtr, byte classJobId)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.classJobChangeArgs.Agent = thisPtr;
|
||||
this.classJobChangeArgs.AgentId = this.agentId;
|
||||
this.classJobChangeArgs.ClassJobId = classJobId;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreClassJobChange, this.classJobChangeArgs);
|
||||
|
||||
classJobId = this.classJobChangeArgs.ClassJobId;
|
||||
|
||||
try
|
||||
{
|
||||
this.OriginalVirtualTable->OnClassJobChange(thisPtr, classJobId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnClassJobChange. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostClassJobChange, this.classJobChangeArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnClassJobChange.");
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
|
||||
{
|
||||
if (loggingEnabled)
|
||||
{
|
||||
// Manually disable the really spammy log events, you can comment this out if you need to debug them.
|
||||
if (caller is "OnAgentUpdate" || this.agentId is AgentId.PadMouseMode)
|
||||
return;
|
||||
|
||||
Log.Debug($"[{caller}]: {this.agentId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -12,7 +14,7 @@ public abstract class BaseAddressResolver
|
|||
/// <summary>
|
||||
/// Gets a list of memory addresses that were found, to list in /xldata.
|
||||
/// </summary>
|
||||
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
|
||||
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(ISigScanner)"/> or <see cref="Setup64Bit(ISigScanner)"/>.
|
||||
|
|
|
|||
221
Dalamud/Game/Chat/LogMessage.cs
Normal file
221
Dalamud/Game/Chat/LogMessage.cs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using FFXIVClientStructs.FFXIV.Component.Text;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using Lumina.Excel;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace Dalamud.Game.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a log message.
|
||||
/// </summary>
|
||||
public interface ILogMessage : IEquatable<ILogMessage>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the log message in memory.
|
||||
/// </summary>
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of this log message.
|
||||
/// </summary>
|
||||
uint LogMessageId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GameData associated with this log message.
|
||||
/// </summary>
|
||||
RowRef<Lumina.Excel.Sheets.LogMessage> GameData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity that is the source of this log message, if any.
|
||||
/// </summary>
|
||||
ILogMessageEntity? SourceEntity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity that is the target of this log message, if any.
|
||||
/// </summary>
|
||||
ILogMessageEntity? TargetEntity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of parameters.
|
||||
/// </summary>
|
||||
int ParameterCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of a parameter for the log message if it is an int.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the parameter to retrieve.</param>
|
||||
/// <param name="value">The value of the parameter.</param>
|
||||
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
|
||||
bool TryGetIntParameter(int index, out int value);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of a parameter for the log message if it is a string.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the parameter to retrieve.</param>
|
||||
/// <param name="value">The value of the parameter.</param>
|
||||
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
|
||||
bool TryGetStringParameter(int index, out ReadOnlySeString value);
|
||||
|
||||
/// <summary>
|
||||
/// Formats this log message into an approximation of the string that will eventually be shown in the log.
|
||||
/// </summary>
|
||||
/// <remarks>This can cause side effects such as playing sound effects and thus should only be used for debugging.</remarks>
|
||||
/// <returns>The formatted string.</returns>
|
||||
ReadOnlySeString FormatLogMessageForDebugging();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This struct represents log message in the queue to be added to the chat.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the log message.</param>
|
||||
internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint LogMessageId => ptr->LogMessageId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.LogMessage> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.LogMessage>(ptr->LogMessageId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
|
||||
|
||||
/// <inheritdoc/>
|
||||
ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int ParameterCount => ptr->Parameters.Count;
|
||||
|
||||
private LogMessageEntity SourceEntity => new(ptr, true);
|
||||
|
||||
private LogMessageEntity TargetEntity => new(ptr, false);
|
||||
|
||||
public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(LogMessage x, LogMessage y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ILogMessage? other)
|
||||
{
|
||||
return other is LogMessage logMessage && this.Equals(logMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is LogMessage logMessage && this.Equals(logMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetIntParameter(int index, out int value)
|
||||
{
|
||||
value = 0;
|
||||
if (!this.TryGetParameter(index, out var parameter)) return false;
|
||||
if (parameter.Type != TextParameterType.Integer) return false;
|
||||
value = parameter.IntValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetStringParameter(int index, out ReadOnlySeString value)
|
||||
{
|
||||
value = default;
|
||||
if (!this.TryGetParameter(index, out var parameter)) return false;
|
||||
if (parameter.Type == TextParameterType.String)
|
||||
{
|
||||
value = new(parameter.StringValue.AsSpan());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parameter.Type == TextParameterType.ReferencedUtf8String)
|
||||
{
|
||||
value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlySeString FormatLogMessageForDebugging()
|
||||
{
|
||||
var logModule = RaptureLogModule.Instance();
|
||||
|
||||
// the formatting logic is taken from RaptureLogModule_Update
|
||||
|
||||
using var utf8 = new Utf8String();
|
||||
SetName(logModule, this.SourceEntity);
|
||||
SetName(logModule, this.TargetEntity);
|
||||
|
||||
using var rssb = new RentedSeStringBuilder();
|
||||
logModule->RaptureTextModule->FormatString(rssb.Builder.Append(this.GameData.Value.Text).GetViewAsSpan(), &ptr->Parameters, &utf8);
|
||||
|
||||
return new ReadOnlySeString(utf8.AsSpan());
|
||||
|
||||
static void SetName(RaptureLogModule* self, LogMessageEntity item)
|
||||
{
|
||||
var name = item.NameSpan.GetPointer(0);
|
||||
|
||||
if (item.IsPlayer)
|
||||
{
|
||||
var str = self->TempParseMessage.GetPointer(item.IsSourceEntity ? 8 : 9);
|
||||
self->FormatPlayerLink(name, str, null, 0, item.Kind != 1 /* LocalPlayer */, item.HomeWorldId, false, null, false);
|
||||
|
||||
if (item.HomeWorldId != 0 && item.HomeWorldId != AgentLobby.Instance()->LobbyData.HomeWorldId)
|
||||
{
|
||||
var crossWorldSymbol = self->RaptureTextModule->UnkStrings0.GetPointer(3);
|
||||
if (!crossWorldSymbol->StringPtr.HasValue)
|
||||
self->RaptureTextModule->ProcessMacroCode(crossWorldSymbol, "<icon(88)>\0"u8);
|
||||
str->Append(crossWorldSymbol);
|
||||
if (self->UIModule->GetWorldHelper()->AllWorlds.TryGetValuePointer(item.HomeWorldId, out var world))
|
||||
str->ConcatCStr(world->Name);
|
||||
}
|
||||
|
||||
name = str->StringPtr;
|
||||
}
|
||||
|
||||
if (item.IsSourceEntity)
|
||||
{
|
||||
self->RaptureTextModule->SetGlobalTempEntity1(name, item.Sex, item.ObjStrId);
|
||||
}
|
||||
else
|
||||
{
|
||||
self->RaptureTextModule->SetGlobalTempEntity2(name, item.Sex, item.ObjStrId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetParameter(int index, out TextParameter value)
|
||||
{
|
||||
if (index < 0 || index >= ptr->Parameters.Count)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = ptr->Parameters[index];
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool Equals(LogMessage other)
|
||||
{
|
||||
return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity;
|
||||
}
|
||||
}
|
||||
113
Dalamud/Game/Chat/LogMessageEntity.cs
Normal file
113
Dalamud/Game/Chat/LogMessageEntity.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace Dalamud.Game.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing an entity related to a log message.
|
||||
/// </summary>
|
||||
public interface ILogMessageEntity : IEquatable<ILogMessageEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of this entity.
|
||||
/// </summary>
|
||||
ReadOnlySeString Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the homeworld of this entity, if it is a player.
|
||||
/// </summary>
|
||||
ushort HomeWorldId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the homeworld of this entity, if it is a player.
|
||||
/// </summary>
|
||||
RowRef<World> HomeWorld { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ObjStr ID of this entity, if not a player. See <seealso cref="ISeStringEvaluator.EvaluateObjStr"/>.
|
||||
/// </summary>
|
||||
uint ObjStrId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this entity is a player.
|
||||
/// </summary>
|
||||
bool IsPlayer { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This struct represents an entity related to a log message.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the log message item.</param>
|
||||
/// <param name="source">If <see langword="true"/> represents the source entity of the log message, otherwise represents the target entity.</param>
|
||||
internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlySeString Name => new(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.HomeWorldId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Span containing the raw name of this entity.
|
||||
/// </summary>
|
||||
internal Span<byte> NameSpan => source ? ptr->SourceName : ptr->TargetName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind of the entity.
|
||||
/// </summary>
|
||||
internal byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Sex of this entity.
|
||||
/// </summary>
|
||||
internal byte Sex => source ? ptr->SourceSex : ptr->TargetSex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this entity is the source entity of a log message.
|
||||
/// </summary>
|
||||
internal bool IsSourceEntity => source;
|
||||
|
||||
public static bool operator ==(LogMessageEntity x, LogMessageEntity y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(LogMessageEntity x, LogMessageEntity y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ILogMessageEntity other)
|
||||
{
|
||||
return other is LogMessageEntity entity && this.Equals(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is LogMessageEntity entity && this.Equals(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Name, this.HomeWorldId, this.ObjStrId, this.Sex, this.IsPlayer);
|
||||
}
|
||||
|
||||
private bool Equals(LogMessageEntity other)
|
||||
{
|
||||
return this.Name == other.Name && this.HomeWorldId == other.HomeWorldId && this.ObjStrId == other.ObjStrId && this.Kind == other.Kind && this.Sex == other.Sex && this.IsPlayer == other.IsPlayer;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using CheapLoc;
|
||||
|
|
@ -23,7 +22,7 @@ namespace Dalamud.Game;
|
|||
[ServiceManager.EarlyLoadedService]
|
||||
internal partial class ChatHandlers : IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("ChatHandlers");
|
||||
private static readonly ModuleLog Log = ModuleLog.Create<ChatHandlers>();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
|
@ -77,7 +76,7 @@ internal partial class ChatHandlers : IServiceType
|
|||
}
|
||||
|
||||
// For injections while logged in
|
||||
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
|
||||
if (clientState.IsLoggedIn && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
|
||||
this.PrintWelcomeMessage();
|
||||
|
||||
#if !DEBUG && false
|
||||
|
|
@ -104,7 +103,7 @@ internal partial class ChatHandlers : IServiceType
|
|||
|
||||
if (this.configuration.PrintDalamudWelcomeMsg)
|
||||
{
|
||||
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion())
|
||||
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion())
|
||||
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +115,7 @@ internal partial class ChatHandlers : IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion))
|
||||
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion))
|
||||
{
|
||||
var linkPayload = chatGui.AddChatLinkHandler(
|
||||
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
|
||||
|
|
@ -124,11 +123,11 @@ internal partial class ChatHandlers : IServiceType
|
|||
var updateMessage = new SeStringBuilder()
|
||||
.AddText(Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully!"))
|
||||
.AddUiForeground(500)
|
||||
.AddText(" [")
|
||||
.AddText(" [ ")
|
||||
.Add(linkPayload)
|
||||
.AddText(Loc.Localize("DalamudClickToViewChangelogs", " Click here to view the changelog."))
|
||||
.AddText(Loc.Localize("DalamudClickToViewChangelogs", "Click here to view the changelog."))
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.AddText("]")
|
||||
.AddText(" ]")
|
||||
.AddUiForegroundOff();
|
||||
|
||||
chatGui.Print(new XivChatEntry
|
||||
|
|
@ -137,7 +136,7 @@ internal partial class ChatHandlers : IServiceType
|
|||
Type = XivChatType.Notice,
|
||||
});
|
||||
|
||||
this.configuration.LastVersion = Util.AssemblyVersion;
|
||||
this.configuration.LastVersion = Versioning.GetAssemblyVersion();
|
||||
this.configuration.QueueSave();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,47 +63,37 @@ public interface IAetheryteEntry
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class representing an aetheryte entry available to the game.
|
||||
/// This struct represents an aetheryte entry available to the game.
|
||||
/// </summary>
|
||||
internal sealed class AetheryteEntry : IAetheryteEntry
|
||||
/// <param name="data">Data read from the Aetheryte List.</param>
|
||||
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
|
||||
{
|
||||
private readonly TeleportInfo data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Data read from the Aetheryte List.</param>
|
||||
internal AetheryteEntry(TeleportInfo data)
|
||||
{
|
||||
this.data = data;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => data.AetheryteId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => this.data.AetheryteId;
|
||||
public uint TerritoryId => data.TerritoryId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint TerritoryId => this.data.TerritoryId;
|
||||
public byte SubIndex => data.SubIndex;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte SubIndex => this.data.SubIndex;
|
||||
public byte Ward => data.Ward;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Ward => this.data.Ward;
|
||||
public byte Plot => data.Plot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Plot => this.data.Plot;
|
||||
public uint GilCost => data.GilCost;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint GilCost => this.data.GilCost;
|
||||
public bool IsFavourite => data.IsFavourite;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsFavourite => this.data.IsFavourite;
|
||||
public bool IsSharedHouse => data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSharedHouse => this.data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsApartment => this.data.IsApartment;
|
||||
public bool IsApartment => data.IsApartment;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Aetherytes;
|
||||
|
|
@ -22,7 +24,7 @@ namespace Dalamud.Game.ClientState.Aetherytes;
|
|||
internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ClientState clientState = Service<ClientState>.Get();
|
||||
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
|
||||
|
||||
private readonly Telepo* telepoInstance = Telepo.Instance();
|
||||
|
||||
|
|
@ -37,7 +39,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
|
|||
{
|
||||
get
|
||||
{
|
||||
if (this.clientState.LocalPlayer == null)
|
||||
if (this.objectTable.LocalPlayer == null)
|
||||
return 0;
|
||||
|
||||
this.Update();
|
||||
|
|
@ -59,7 +61,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
|
|||
return null;
|
||||
}
|
||||
|
||||
if (this.clientState.LocalPlayer == null)
|
||||
if (this.objectTable.LocalPlayer == null)
|
||||
return null;
|
||||
|
||||
return new AetheryteEntry(this.telepoInstance->TeleportList[index]);
|
||||
|
|
@ -69,7 +71,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
|
|||
private void Update()
|
||||
{
|
||||
// this is very very important as otherwise it crashes
|
||||
if (this.clientState.LocalPlayer == null)
|
||||
if (this.objectTable.LocalPlayer == null)
|
||||
return;
|
||||
|
||||
this.telepoInstance->UpdateAetheryteList();
|
||||
|
|
@ -87,10 +89,7 @@ internal sealed partial class AetheryteList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IAetheryteEntry> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -98,4 +97,34 @@ internal sealed partial class AetheryteList
|
|||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
private struct Enumerator(AetheryteList aetheryteList) : IEnumerator<IAetheryteEntry>
|
||||
{
|
||||
private int index = -1;
|
||||
|
||||
public IAetheryteEntry Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (++this.index < aetheryteList.Length)
|
||||
{
|
||||
this.Current = aetheryteList[this.index];
|
||||
return true;
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Game.Player;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
||||
|
|
@ -21,10 +24,10 @@ namespace Dalamud.Game.ClientState.Buddy;
|
|||
#pragma warning restore SA1015
|
||||
internal sealed partial class BuddyList : IServiceType, IBuddyList
|
||||
{
|
||||
private const uint InvalidObjectID = 0xE0000000;
|
||||
private const uint InvalidEntityId = 0xE0000000;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ClientState clientState = Service<ClientState>.Get();
|
||||
private readonly PlayerState playerState = Service<PlayerState>.Get();
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private BuddyList()
|
||||
|
|
@ -69,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
|
||||
private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IBuddyMember? this[int index]
|
||||
|
|
@ -82,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetCompanionBuddyMemberAddress()
|
||||
public unsafe nint GetCompanionBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
return (nint)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetPetBuddyMemberAddress()
|
||||
public unsafe nint GetPetBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
|
||||
return (nint)this.BuddyListStruct->PetInfo.Pet;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
|
||||
public unsafe nint GetBattleBuddyMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= 3)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
|
||||
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
|
||||
{
|
||||
if (this.clientState.LocalContentId == 0)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (this.playerState.ContentId == 0)
|
||||
return null;
|
||||
|
||||
var buddy = new BuddyMember(address);
|
||||
if (buddy.ObjectId == InvalidObjectID)
|
||||
var buddy = new BuddyMember((CSBuddyMember*)address);
|
||||
if (buddy.EntityId == InvalidEntityId)
|
||||
return null;
|
||||
|
||||
return buddy;
|
||||
|
|
@ -130,12 +133,39 @@ internal sealed partial class BuddyList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IBuddyMember> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(BuddyList buddyList) : IEnumerator<IBuddyMember>
|
||||
{
|
||||
private int index = -1;
|
||||
|
||||
public IBuddyMember Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (++this.index < buddyList.Length)
|
||||
{
|
||||
this.Current = buddyList[this.index];
|
||||
return true;
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,36 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
public interface IBuddyMember
|
||||
public interface IBuddyMember : IEquatable<IBuddyMember>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the buddy in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object ID of this buddy.
|
||||
/// </summary>
|
||||
[Obsolete("Renamed to EntityId")]
|
||||
uint ObjectId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity ID of this buddy.
|
||||
/// </summary>
|
||||
uint EntityId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actor associated with this buddy.
|
||||
/// </summary>
|
||||
|
|
@ -61,39 +71,34 @@ public interface IBuddyMember
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
internal unsafe class BuddyMember : IBuddyMember
|
||||
/// <param name="ptr">A pointer to the BuddyMember.</param>
|
||||
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuddyMember"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Buddy address.</param>
|
||||
internal BuddyMember(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntPtr Address { get; }
|
||||
public uint ObjectId => this.EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint ObjectId => this.Struct->EntityId;
|
||||
public uint EntityId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint CurrentHP => this.Struct->CurrentHealth;
|
||||
public uint CurrentHP => ptr->CurrentHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint MaxHP => this.Struct->MaxHealth;
|
||||
public uint MaxHP => ptr->MaxHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint DataID => this.Struct->DataId;
|
||||
public uint DataID => ptr->DataId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
|
||||
|
|
@ -104,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember
|
|||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
|
||||
public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IBuddyMember? other)
|
||||
{
|
||||
return this.EntityId == other.EntityId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is BuddyMember fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.EntityId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue