mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-16 21:07:45 +01:00
Compare commits
2225 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eec8ee7094 | ||
|
|
13500264b7 | ||
|
|
6ba735eefb | ||
|
|
73f02851a6 | ||
|
|
069323cfb8 | ||
|
|
9aa566f521 | ||
|
|
eff3784a85 | ||
|
|
9cf7030f87 | ||
|
|
deb3686df5 | ||
|
|
953f243caf | ||
|
|
59fec5db82 | ||
|
|
37f3044376 | ||
|
|
fb299d71f0 | ||
|
|
dbcb2e38ec | ||
|
|
ebcbc5d98a | ||
|
|
febced0708 | ||
|
|
4c8ff40821 | ||
|
|
7717251c6a | ||
|
|
3e7511cb34 | ||
|
|
ccb5b01290 | ||
|
|
5dd74297c6 | ||
|
|
338e3bc1a5 | ||
|
|
e240a42a2c | ||
|
|
5be021b0eb | ||
|
|
ce54aa5d25 | ||
|
|
c4b6e4e00b | ||
|
|
912c183fc6 | ||
|
|
5bf901d0c4 | ||
|
|
cbedc878b9 | ||
|
|
c8cf560fc1 | ||
|
|
f05cb52da2 | ||
|
|
7ed81a9823 | ||
|
|
60aa23efcd | ||
|
|
ebbe957c95 | ||
|
|
300e0e6d84 | ||
|
|
049baa4fe4 | ||
|
|
0881dfde8a | ||
|
|
23c0506cb8 | ||
|
|
699745413e | ||
|
|
eb53f04c6b | ||
|
|
c6b596169c | ||
|
|
a0c3e820b0 | ||
|
|
a59689ebfe | ||
|
|
e9f67a009b | ||
|
|
97c8d82b33 | ||
|
|
c3b00ff426 | ||
|
|
6348c4a639 | ||
|
|
5a6e06df3b | ||
|
|
f5f6dd3246 | ||
|
|
4e788f7c2b | ||
|
|
ad1659caf6 | ||
|
|
18a6ce2a5f | ||
|
|
e68e821b2a | ||
|
|
96764b34ca | ||
|
|
2cf60b78cd | ||
|
|
d59be1e660 | ||
|
|
5503bb32e0 | ||
|
|
f3ec4b2e08 | ||
|
|
b3379a9710 | ||
|
|
8c25ef4b47 | ||
|
|
912020cc3f | ||
|
|
be8987a451 | ||
|
|
f7cf5503bb | ||
|
|
a04a5a071c | ||
|
|
71e24c13c7 | ||
|
|
c0120f81af | ||
|
|
da47c19aeb | ||
|
|
e16800f216 | ||
|
|
79a4fc5904 | ||
|
|
bf90725dd2 | ||
|
|
a14347f73a | ||
|
|
1e07e43498 | ||
|
|
f51f8a7bf8 | ||
|
|
1fca78fa71 | ||
|
|
c8b6325a87 | ||
|
|
6079103505 | ||
|
|
d302a17f1f | ||
|
|
0d64384059 | ||
|
|
10894d451a | ||
|
|
fb34238530 | ||
|
|
8043e6fb6b | ||
|
|
e3b7f72893 | ||
|
|
b7f326e29c | ||
|
|
dad01e1af8 | ||
|
|
10b71930a1 | ||
|
|
23257f94a4 | ||
|
|
83a36ed4cb | ||
|
|
8304579d29 | ||
|
|
24cbc6c5e1 | ||
|
|
41edc23820 | ||
|
|
aa920b5e9b | ||
|
|
87ace28bcf | ||
|
|
5917f5fad1 | ||
|
|
f69c264317 | ||
|
|
a7246b9d98 | ||
|
|
9aff388e21 | ||
|
|
091aff1b8a | ||
|
|
9f8185f67b | ||
|
|
b112d75a27 | ||
|
|
7af81a6c18 | ||
|
|
12a218bb2b | ||
|
|
f6bac93db7 | ||
|
|
155d3d49aa | ||
|
|
9aae2210a2 | ||
|
|
3785a629ce | ||
|
|
02af52671f | ||
|
|
391c9d727e | ||
|
|
ff2b2be953 | ||
|
|
6242b30f93 | ||
|
|
11cd08a9de | ||
|
|
46cfbcb115 | ||
|
|
66543cc671 | ||
|
|
13283c9690 | ||
|
|
bedfb22466 | ||
|
|
13df8b2248 | ||
|
|
93406e4d4e | ||
|
|
8140d08557 | ||
|
|
2b36f39848 | ||
|
|
a69811800d | ||
|
|
3f18ad50de | ||
|
|
6689e326ee | ||
|
|
bdcab22a55 | ||
|
|
f5f4fe7259 | ||
|
|
898963fea5 | ||
|
|
8527bfa29c | ||
|
|
baca3cdec2 | ||
|
|
dc93eba34c | ||
|
|
012052daa0 | ||
|
|
a9546e31ee | ||
|
|
a4a6283e7b | ||
|
|
00c02fd16e | ||
|
|
140d150bb4 | ||
|
|
49a6d935f3 | ||
|
|
692beacc2e | ||
|
|
a953febfba | ||
|
|
c0aa2e36ea | ||
|
|
278bf43b29 | ||
|
|
a97d9e4953 | ||
|
|
30e3cd1f38 | ||
|
|
62e9dc164d | ||
|
|
9fc572ba0c | ||
|
|
3c20b541ce | ||
|
|
1961b03d37 | ||
|
|
1f4ec984b3 | ||
|
|
4981b0348f | ||
|
|
a8c05fc6ee | ||
|
|
3d05662384 | ||
|
|
973814b31b | ||
|
|
a16fd85a7e | ||
|
|
4c0e6d2a67 | ||
|
|
535694e9c8 | ||
|
|
318a41fe52 | ||
|
|
98203e4e8a | ||
|
|
6cba63ac98 | ||
|
|
b48c4f440a | ||
|
|
75f4e66dbf | ||
|
|
74bd1cf911 | ||
|
|
ff2a9f95c4 | ||
|
|
9921c3332e | ||
|
|
f2927290f5 | ||
|
|
1551d9b6f3 | ||
|
|
5e985f4a84 | ||
|
|
2c115eda94 | ||
|
|
ebe45c6a47 | ||
|
|
82fc334be7 | ||
|
|
cd56163b1b | ||
|
|
ccc2c1fd4c | ||
|
|
08c9124858 | ||
|
|
1bdbfe22c1 | ||
|
|
9e7c304556 | ||
|
|
bc4f88aee9 | ||
|
|
400d7d0bea | ||
|
|
ac4c75d3c3 | ||
|
|
507b0a5aee | ||
|
|
f5db888bbd | ||
|
|
d7dee39fab | ||
|
|
3412786282 | ||
|
|
861cbc7759 | ||
|
|
fefa3852f7 | ||
|
|
68b68d6ce7 | ||
|
|
47b5895404 | ||
|
|
e18e4bb0e1 | ||
|
|
6e4e28fa00 | ||
|
|
e326e3d809 | ||
|
|
fbc4c2d054 | ||
|
|
3078c467d0 | ||
|
|
52927ff06b | ||
|
|
08e8b9d2a4 | ||
|
|
f1448ed947 | ||
|
|
c0dcfdd835 | ||
|
|
70295b7a6b | ||
|
|
480942339f | ||
|
|
6ad0b4299a | ||
|
|
0adec35848 | ||
|
|
0fe4a3671a | ||
|
|
363d115be8 | ||
|
|
7595827d29 | ||
|
|
117724b0ae | ||
|
|
a5d221dc13 | ||
|
|
cbebfe5e99 | ||
|
|
0c768979d4 | ||
|
|
53ef42adfa | ||
|
|
0954f50912 | ||
|
|
5d5fc673b1 | ||
|
|
2bd0c89588 | ||
|
|
f03a139e0e | ||
|
|
f9b5a626cf | ||
|
|
dc336569ff | ||
|
|
0ec6a17ac7 | ||
|
|
129156a1c1 | ||
|
|
33ada1d994 | ||
|
|
0afcae4504 | ||
|
|
93e60471de | ||
|
|
5437ab477f | ||
|
|
3b54485127 | ||
|
|
c3b2443ab5 | ||
|
|
2fdafc5c85 | ||
|
|
09c2264de4 | ||
|
|
c3be151d40 | ||
|
|
abb47751c8 | ||
|
|
1d517103b3 | ||
|
|
fe5d1bc36e | ||
|
|
b589103b05 | ||
|
|
cc76125b1c | ||
|
|
f3bcc4d554 | ||
|
|
2dd6dd201c | ||
|
|
cb0214ca2f | ||
|
|
5a5a1487a3 | ||
|
|
de408e4d58 | ||
|
|
a1bf26e7e8 | ||
|
|
3bb7db10fb | ||
|
|
8a68a1bff5 | ||
|
|
01e6f58463 | ||
|
|
7498bc469f | ||
|
|
23ba77c107 | ||
|
|
1a1d1c1840 | ||
|
|
b019da2a8c | ||
|
|
60becf0a09 | ||
|
|
974b215610 | ||
|
|
8e191ae075 | ||
|
|
b189ac027b | ||
|
|
6cbc8bd58f | ||
|
|
49f077aca0 | ||
|
|
525d1c6bf9 | ||
|
|
124b54ab04 | ||
|
|
b8b2127a5d | ||
|
|
586bd9d0cc | ||
|
|
03bb07a9c0 | ||
|
|
279a861582 | ||
|
|
82a1271281 | ||
|
|
26a6cc4735 | ||
|
|
61d70f7b4e | ||
|
|
0213096c58 | ||
|
|
dc47a08988 | ||
|
|
83574dfeb1 | ||
|
|
87f44d7a88 | ||
|
|
cda6a4c420 | ||
|
|
4093228e61 | ||
|
|
442ae960cf | ||
|
|
e7f7077e96 | ||
|
|
e5620e17e0 | ||
|
|
93b0996794 | ||
|
|
1d70be8060 | ||
|
|
eab98ec0e4 | ||
|
|
861b7b78cd | ||
|
|
6eacc82dcd | ||
|
|
1afbbfef78 | ||
|
|
7cf0367361 | ||
|
|
0b0c92eb09 | ||
|
|
34d51b66aa | ||
|
|
cda9b1df65 | ||
|
|
509f11561a | ||
|
|
13adbd5466 | ||
|
|
26985e01a2 | ||
|
|
deba8ac910 | ||
|
|
1ebe4099d6 | ||
|
|
c6de7ddebd | ||
|
|
8860d1e39a | ||
|
|
2413424c8a | ||
|
|
9b25193d4e | ||
|
|
70844610d8 | ||
|
|
e4cfd674ee | ||
|
|
776a93dc73 | ||
|
|
514b0e7f30 | ||
|
|
4a00d82921 | ||
|
|
fdd75e2866 | ||
|
|
b2860c1047 | ||
|
|
1f172b4632 | ||
|
|
d40c59eee9 | ||
|
|
f8d0616acd | ||
|
|
31f23024a4 | ||
|
|
6d2b72e079 | ||
|
|
b76626ac8d | ||
|
|
579969a9e1 | ||
|
|
2f0bf19d00 | ||
|
|
ef26049c53 | ||
|
|
a73dee83b3 | ||
|
|
41672c31ce | ||
|
|
a561e70410 | ||
|
|
b7b9defaa6 | ||
|
|
79938b6dd0 | ||
|
|
40f24344af | ||
|
|
93e184c9a5 | ||
|
|
2be5bd0611 | ||
|
|
f89eab8b2b | ||
|
|
a9a556eb55 | ||
|
|
0af9667789 | ||
|
|
60b9facea3 | ||
|
|
50c4207844 | ||
|
|
9b18ffce66 | ||
|
|
214be98662 | ||
|
|
f9952ada75 | ||
|
|
3ba2563e0b | ||
|
|
4cc5041f0a | ||
|
|
f9b163e7c5 | ||
|
|
981c2bace4 | ||
|
|
ec09a7eb0e | ||
|
|
7022b37043 | ||
|
|
64748790cc | ||
|
|
b0a8b1baa5 | ||
|
|
ac64b4db24 | ||
|
|
e508e6158f | ||
|
|
4d26a63944 | ||
|
|
30a957356a | ||
|
|
9ab8985343 | ||
|
|
a3ddce0ef5 | ||
|
|
0159eb3d83 | ||
|
|
55ce633832 | ||
|
|
40168d7daf | ||
|
|
dcab443b2f | ||
|
|
dcc4354777 | ||
|
|
2afd6b966e | ||
|
|
737e74582b | ||
|
|
39c73af238 | ||
|
|
9ca0145a7f | ||
|
|
0c8571fba9 | ||
|
|
8779f4b689 | ||
|
|
7b517390b6 | ||
|
|
7d75c7d7a5 | ||
|
|
4f0428832c | ||
|
|
b62563d721 | ||
|
|
ec3ec7db4e | ||
|
|
5f8377acaa | ||
|
|
3b8aac8eca | ||
|
|
a1931a93fb | ||
|
|
df148b556a | ||
|
|
bdc2da95c4 | ||
|
|
1462891bd3 | ||
|
|
d2a8cec01f | ||
|
|
795fa7336e | ||
|
|
e77fa18c61 | ||
|
|
9559bd7358 | ||
|
|
9c25fab183 | ||
|
|
cc981eba15 | ||
|
|
30a4b90e84 | ||
|
|
82689467aa | ||
|
|
415e15f3b1 | ||
|
|
3687c99ee6 | ||
|
|
6ea38eac0a | ||
|
|
7f52777fd4 | ||
|
|
2753c786fc | ||
|
|
7b2e82b27f | ||
|
|
aebd22ed64 | ||
|
|
c99a7884bb | ||
|
|
e73b3e85bd | ||
|
|
0758739666 | ||
|
|
d4e6688369 | ||
|
|
e6872cff64 | ||
|
|
b83564bce8 | ||
|
|
e8300fc5c8 | ||
|
|
f07780cf7b | ||
|
|
349241d0ab | ||
|
|
1845c4b89b | ||
|
|
756537c776 | ||
|
|
9a457a1a95 | ||
|
|
af7a8fbddd | ||
|
|
0eed5f1707 | ||
|
|
6374362b28 | ||
|
|
7da5d73b47 | ||
|
|
a2258e6160 | ||
|
|
dbef1cccb2 | ||
|
|
653f6269b7 | ||
|
|
a5d8baebca | ||
|
|
cff482a2ed | ||
|
|
5f9cbe9ab1 | ||
|
|
282189ef6d | ||
|
|
98a89bb2b4 | ||
|
|
67305d507a | ||
|
|
fbbfe5e00d | ||
|
|
7a2691b942 | ||
|
|
50b5eeb700 | ||
|
|
2483f3dcdf | ||
|
|
0e2364497f | ||
|
|
25d0a2c9a8 | ||
|
|
f24056ea31 | ||
|
|
b3883c1306 | ||
|
|
f679e0ccee | ||
|
|
d5e575423b | ||
|
|
18288815b2 | ||
|
|
cc97ea0ce9 | ||
|
|
b5a469c524 | ||
|
|
510b9a5f1f | ||
|
|
5db3d53994 | ||
|
|
08ff9b679e | ||
|
|
22c3b3b629 | ||
|
|
4cc7d1930b | ||
|
|
01db37cbd4 | ||
|
|
e9014fe4c3 | ||
|
|
1434ad6190 | ||
|
|
22541b3fd8 | ||
|
|
b377ca372c | ||
|
|
d7095af89b | ||
|
|
8b9f59426e | ||
|
|
97d7ea7759 | ||
|
|
9787e5a852 | ||
|
|
242c0ee38f | ||
|
|
c8ad4bc106 | ||
|
|
ac2631384f | ||
|
|
0aa8a44b8d | ||
|
|
8242cde15c | ||
|
|
28250a9304 | ||
|
|
10279fdc18 | ||
|
|
b1be868a6a | ||
|
|
65538868c3 | ||
|
|
cc49bdcb36 | ||
|
|
d2a015f32a | ||
|
|
d0e0ae46e6 | ||
|
|
9822ab4128 | ||
|
|
25aac1a03e | ||
|
|
5a46361d4f | ||
|
|
17d8826ae9 | ||
|
|
22be9f2d07 | ||
|
|
06ba0ba956 | ||
|
|
234130cf86 | ||
|
|
977cb2196a | ||
|
|
37332c432b | ||
|
|
ee48ea0166 | ||
|
|
f2bdaf1b49 | ||
|
|
9e72432682 | ||
|
|
ce75471e51 | ||
|
|
688b84141f | ||
|
|
3beef61c6f | ||
|
|
11d0cfd1e2 | ||
|
|
8a53313c33 | ||
|
|
0928d712c9 | ||
|
|
41718d8f8f | ||
|
|
7ab5299f7a | ||
|
|
597380355a | ||
|
|
a864ac1965 | ||
|
|
83e5feb7db | ||
|
|
5599f12753 | ||
|
|
e3a1ae6938 | ||
|
|
c54141be54 | ||
|
|
7dfc564a4c | ||
|
|
d50fbf5a1c | ||
|
|
ed717c69f9 | ||
|
|
c4f6038d1e | ||
|
|
2358eb378d | ||
|
|
7e6ea5008c | ||
|
|
69971c12af | ||
|
|
71101ef553 | ||
|
|
472d803141 | ||
|
|
9ddb011545 | ||
|
|
339d1f8caf | ||
|
|
9bd1f86a1d | ||
|
|
50db83146a | ||
|
|
a54e45f9c3 | ||
|
|
e646b48afa | ||
|
|
97b310ca3f | ||
|
|
db2ce1328f | ||
|
|
1d5a7a41ab | ||
|
|
2c5ffc1bc5 | ||
|
|
40c772a9da | ||
|
|
4a0c996ff6 | ||
|
|
2e424a693d | ||
|
|
c4b59295cb | ||
|
|
740816f3a6 | ||
|
|
df0526e6e5 | ||
|
|
76c0264cbe | ||
|
|
a1a880a0f4 | ||
|
|
3b21de35cc | ||
|
|
efd08ae053 | ||
|
|
8468ed2c07 | ||
|
|
8fa0875ec6 | ||
|
|
4719f413b6 | ||
|
|
5258c600b7 | ||
|
|
3e90524b06 | ||
|
|
9c6498e028 | ||
|
|
9de6b3a905 | ||
|
|
fecdee05bd | ||
|
|
8084f48144 | ||
|
|
389c42e68f | ||
|
|
776b4e9efb | ||
|
|
caf4382e1f | ||
|
|
22aca49112 | ||
|
|
af2a14826c | ||
|
|
9b958a9d37 | ||
|
|
00fbb2686b | ||
|
|
ac1ea124d9 | ||
|
|
26371d42f7 | ||
|
|
10ce5da8c9 | ||
|
|
0c6d777c75 | ||
|
|
bd59591ed8 | ||
|
|
22cbecc6a4 | ||
|
|
1b17404876 | ||
|
|
6b858dc5ac | ||
|
|
75858a61b5 | ||
|
|
04582ba00b | ||
|
|
fb144d0b74 | ||
|
|
ff3e5410aa | ||
|
|
176001195b | ||
|
|
2a7d2ef0d5 | ||
|
|
de3644e9e1 | ||
|
|
5c5e45114f | ||
|
|
e5ff9cee9e | ||
|
|
f5e6132462 | ||
|
|
f8e3b6777f | ||
|
|
f043311882 | ||
|
|
4117d45d15 | ||
|
|
6d408ba695 | ||
|
|
4970e57131 | ||
|
|
d713d5a112 | ||
|
|
a3c22f2826 | ||
|
|
233a999650 | ||
|
|
3e2c9177a7 | ||
|
|
ded910d8a1 | ||
|
|
c4853434c8 | ||
|
|
f3346c5d7e | ||
|
|
726340e4f8 | ||
|
|
a2237773e3 | ||
|
|
3549283769 | ||
|
|
96f0479b53 | ||
|
|
1da095be99 | ||
|
|
bedf5dab79 | ||
|
|
bb9dd184a3 | ||
|
|
3135d5e7e6 | ||
|
|
6e351aa68b | ||
|
|
8ee326853d | ||
|
|
5663822b2b | ||
|
|
47268ab377 | ||
|
|
6b0b1629bd | ||
|
|
7710d92496 | ||
|
|
a27ce6b0a7 | ||
|
|
5c9e158da3 | ||
|
|
ccce087b87 | ||
|
|
7ba7a6e319 | ||
|
|
421fde70b0 | ||
|
|
741141f227 | ||
|
|
1b671b95ab | ||
|
|
b5f7f03e11 | ||
|
|
6c0d8ea889 | ||
|
|
e44b450548 | ||
|
|
465e65e8fe | ||
|
|
a52a43bd86 | ||
|
|
b3d841a8ec | ||
|
|
c265b917b4 | ||
|
|
03e9dc55df | ||
|
|
fb58a9c271 | ||
|
|
d630a3dff4 | ||
|
|
1648bfe424 | ||
|
|
fe4a046cc9 | ||
|
|
f0c034c84d | ||
|
|
df58ac7e92 | ||
|
|
b8a3a854bd | ||
|
|
a40a5d343f | ||
|
|
2bf08c8c89 | ||
|
|
1187efa243 | ||
|
|
0d1ed6a926 | ||
|
|
f68e919421 | ||
|
|
a36f9ccec7 | ||
|
|
dba85f5da3 | ||
|
|
700fef4f04 | ||
|
|
1b5553284c | ||
|
|
2534f119e9 | ||
|
|
a585976190 | ||
|
|
0064c4c96e | ||
|
|
e91e0b23f8 | ||
|
|
0dbf718340 | ||
|
|
6d42673aa4 | ||
|
|
f2094c2c58 | ||
|
|
f8b034c42d | ||
|
|
f3ab1ddbb4 | ||
|
|
c8e859ae05 | ||
|
|
728a081419 | ||
|
|
30d10d5a26 | ||
|
|
d90c3dd1af | ||
|
|
ee086e3e76 | ||
|
|
a6ee4c96ea | ||
|
|
da3f3b8df3 | ||
|
|
75e3ef72f3 | ||
|
|
243593e30f | ||
|
|
c849e31034 | ||
|
|
c01aa000fb | ||
|
|
5323add662 | ||
|
|
f4fe3605f0 | ||
|
|
36ab9573ae | ||
|
|
450751e43f | ||
|
|
e8182f285e | ||
|
|
59b3859f11 | ||
|
|
60986c78f8 | ||
|
|
069b28272b | ||
|
|
4454ac48da | ||
|
|
6241187431 | ||
|
|
f3e7271157 | ||
|
|
d903f1b8c3 | ||
|
|
5e9c7f7eac | ||
|
|
1e1637f0e7 | ||
|
|
7579eaacbe | ||
|
|
73b9d1fca0 | ||
|
|
a308fb9f77 | ||
|
|
9e15865a99 | ||
|
|
67a220f821 | ||
|
|
4b9870f090 | ||
|
|
d247f83e1d | ||
|
|
9d128a4d83 | ||
|
|
70281c576e | ||
|
|
5270ad4d0d | ||
|
|
8518240bf9 | ||
|
|
7ceaeb826f | ||
|
|
ee801b637e | ||
|
|
5512e0cad2 | ||
|
|
3754f57132 | ||
|
|
e52b027545 | ||
|
|
d30d418afe | ||
|
|
5d50523c72 | ||
|
|
af0bbeb8bf | ||
|
|
4806f8dc3e | ||
|
|
bb4665c367 | ||
|
|
d0c4d6984c | ||
|
|
19166d8cf4 | ||
|
|
a1a7487897 | ||
|
|
cbf5baf65c | ||
|
|
6f3d9eb272 | ||
|
|
f143601aa0 | ||
|
|
3ffe6151ff | ||
|
|
246f4f65f5 | ||
|
|
72f2834dfd | ||
|
|
c501d0b365 | ||
|
|
30f9233862 | ||
|
|
df33557477 | ||
|
|
528e3226b5 | ||
|
|
a4cd5695fb | ||
|
|
ca648f98a1 | ||
|
|
29dce8f3ab | ||
|
|
1501bd4fbf | ||
|
|
cec28a1823 | ||
|
|
07382537a0 | ||
|
|
ceaa9ca29a | ||
|
|
ee5a21f7a2 | ||
|
|
0db70c89b1 | ||
|
|
8351b74b21 | ||
|
|
8c34c18643 | ||
|
|
48ab98bee6 | ||
|
|
c3b7ddad28 | ||
|
|
5b1c0cf0e3 | ||
|
|
e2313ba925 | ||
|
|
258f7e9732 | ||
|
|
a4548bbf04 | ||
|
|
f533ae6667 | ||
|
|
1a8d194e05 | ||
|
|
f978b35b76 | ||
|
|
defba19b2d | ||
|
|
5abbd8b110 | ||
|
|
9bba1e2b31 | ||
|
|
6d0562180a | ||
|
|
e7c786b239 | ||
|
|
1922353ba3 | ||
|
|
c9379b6d60 | ||
|
|
4824a96ab0 | ||
|
|
ad877e68e6 | ||
|
|
67a35b9abb | ||
|
|
eb784dddf0 | ||
|
|
89cbb3f60d | ||
|
|
519b3d4891 | ||
|
|
78af40d507 | ||
|
|
c98bee67a5 | ||
|
|
d952d83adf | ||
|
|
12dfaaef99 | ||
|
|
9c781f8563 | ||
|
|
07c3be641d | ||
|
|
e46fcc4af1 | ||
|
|
d6f61f06cb | ||
|
|
d815266ed7 | ||
|
|
94a05afbe0 | ||
|
|
22af545e8d | ||
|
|
40be298d67 | ||
|
|
6beb2416dc | ||
|
|
24597d7dc0 | ||
|
|
34cbf37c32 | ||
|
|
380dd0cffb | ||
|
|
1be75444cd | ||
|
|
e2112202a0 | ||
|
|
37ffe52869 | ||
|
|
baa439d246 | ||
|
|
806e001bad | ||
|
|
3b980c1a49 | ||
|
|
1efd493834 | ||
|
|
3c417d7aec | ||
|
|
c2517499f2 | ||
|
|
56502f19f9 | ||
|
|
a0a3435918 | ||
|
|
b677a14cef | ||
|
|
710f39768b | ||
|
|
f89eea721f | ||
|
|
585601efd4 | ||
|
|
56e284a99e | ||
|
|
0d939b12f4 | ||
|
|
4f0f3721a6 | ||
|
|
68135f3757 | ||
|
|
41d271213e | ||
|
|
1284037554 | ||
|
|
4026dd5867 | ||
|
|
9fb8090781 | ||
|
|
431933e9c1 | ||
|
|
221b18751d | ||
|
|
c2e74ed382 | ||
|
|
b07af32dee | ||
|
|
045abc787d | ||
|
|
ab1e11aba1 | ||
|
|
8cd8efa723 | ||
|
|
f686a0ff09 | ||
|
|
29f8c91306 | ||
|
|
a90e253c73 | ||
|
|
90124e83df | ||
|
|
819afc518c | ||
|
|
e05dbe9885 | ||
|
|
cf1dcfcb7c | ||
|
|
f9c45a2f3f | ||
|
|
03d3c38ad5 | ||
|
|
d7a8c9415b | ||
|
|
be729afd4b | ||
|
|
91d9e465ed | ||
|
|
600fd2ecd3 | ||
|
|
9ecc4ab46d | ||
|
|
ebef4ff650 | ||
|
|
943207cae8 | ||
|
|
c53f29c257 | ||
|
|
a7b90639c6 | ||
|
|
a61a96f1ef | ||
|
|
30b32fdcd2 | ||
|
|
ad0c64d4ac | ||
|
|
e33512cf7f | ||
|
|
4ca49598f8 | ||
|
|
3170edfeb6 | ||
|
|
361082813b | ||
|
|
196ca2ce39 | ||
|
|
d9b63320f0 | ||
|
|
0445ed0ef9 | ||
|
|
5ca9e63a2a | ||
|
|
e0339160e9 | ||
|
|
13156a58e9 | ||
|
|
94fdd848b7 | ||
|
|
d7b60206d7 | ||
|
|
250c4034e0 | ||
|
|
b3f8762494 | ||
|
|
ec207bdba2 | ||
|
|
b1a0590382 | ||
|
|
532e8a0936 | ||
|
|
2346b7588a | ||
|
|
447735f609 | ||
|
|
c8ea33f8dd | ||
|
|
863a7edf0e | ||
|
|
30a87e3f40 | ||
|
|
ecd5752d16 | ||
|
|
f7adc83d63 | ||
|
|
f6b35497c5 | ||
|
|
f51fc2cafd | ||
|
|
159942f29c | ||
|
|
e884b269a9 | ||
|
|
de0309bfa7 | ||
|
|
102e7335a7 | ||
|
|
50a7e7efb7 | ||
|
|
2e9f184454 | ||
|
|
ceed8531af | ||
|
|
03bfbcc309 | ||
|
|
afdffa4f2c | ||
|
|
48dd4bcadb | ||
|
|
87fec7783e | ||
|
|
699ae8e1fb | ||
|
|
aeb2db9f5d | ||
|
|
63b3a02e95 | ||
|
|
b63935e81e | ||
|
|
05d010a281 | ||
|
|
137b752196 | ||
|
|
3b81dd89c8 | ||
|
|
3deda68eec | ||
|
|
3b26e97231 | ||
|
|
2e6473dc09 | ||
|
|
ef9d81c061 | ||
|
|
cfa58ee196 | ||
|
|
e7cf9d35c9 | ||
|
|
5101b73fdc | ||
|
|
aba68cfb92 | ||
|
|
331b7fbc1d | ||
|
|
24d4e9fac6 | ||
|
|
b79600ea14 | ||
|
|
ce11bec985 | ||
|
|
f61bd8bb8a | ||
|
|
67bb95f6e6 | ||
|
|
81fdbf6ccf | ||
|
|
c7046ec006 | ||
|
|
f4bdbcac53 | ||
|
|
a6661f15e8 | ||
|
|
b2e1bff782 | ||
|
|
09742e2e50 | ||
|
|
8891ea0570 | ||
|
|
f5d6ac8bdb | ||
|
|
5d1b17f96d | ||
|
|
255d11974f | ||
|
|
eb2a9b8109 | ||
|
|
b30de460e7 | ||
|
|
f11cefcec1 | ||
|
|
fe266dca31 | ||
|
|
ca777ba1bf | ||
|
|
1d230050c2 | ||
|
|
cd133bddbb | ||
|
|
ed083f2a4c | ||
|
|
f9527970cb | ||
|
|
e32e314863 | ||
|
|
bad1f45ab9 | ||
|
|
4743acf767 | ||
|
|
65627b5002 | ||
|
|
125e5628ec | ||
|
|
992cdff58d | ||
|
|
fca1bf9d94 | ||
|
|
7df9ddcb99 | ||
|
|
dfdd5167a8 | ||
|
|
c06d5b0871 | ||
|
|
2585de8b21 | ||
|
|
e85b84dafe | ||
|
|
bb56faa288 | ||
|
|
df6eb3fdd2 | ||
|
|
d47d31b665 | ||
|
|
32dbf419e2 | ||
|
|
bbbf65eb4c | ||
|
|
1f2f66b114 | ||
|
|
d8dad91e89 | ||
|
|
97166379a7 | ||
|
|
d72008b4b9 | ||
|
|
2a5df2dfb0 | ||
|
|
7553b5da8a | ||
|
|
36fc251d5b | ||
|
|
46a111d152 | ||
|
|
9063d131ba | ||
|
|
078688454a | ||
|
|
c96adcf557 | ||
|
|
2e76148fba | ||
|
|
9084b43e3e | ||
|
|
cff6172453 | ||
|
|
1e5ed1c414 | ||
|
|
616db0dcc3 | ||
|
|
297be487b5 | ||
|
|
e40c4999b6 | ||
|
|
a72be22d3b | ||
|
|
06953c175d | ||
|
|
0fd14ffefc | ||
|
|
72db023804 | ||
|
|
c1472d5f65 | ||
|
|
cd76c31d8c | ||
|
|
514121d8c1 | ||
|
|
6b1743b776 | ||
|
|
07afbfb229 | ||
|
|
792a04337f | ||
|
|
e21c9fb6d1 | ||
|
|
b34114400f | ||
|
|
c276f922a5 | ||
|
|
1bc3bb17c9 | ||
|
|
cc2f72b73d | ||
|
|
11acd7d3f4 | ||
|
|
4a6d94f0fb | ||
|
|
b99a809eba | ||
|
|
f86f29b44a | ||
|
|
2d5afde612 | ||
|
|
9f4c6767f8 | ||
|
|
8fc7de64d9 | ||
|
|
ceb3d39a9a | ||
|
|
75cfffeba7 | ||
|
|
624dd40d58 | ||
|
|
ef1bbb6d9d | ||
|
|
aeb7bd5431 | ||
|
|
dbfaf37800 | ||
|
|
fd1f9b95d6 | ||
|
|
1641166d6e | ||
|
|
0fa62f40d7 | ||
|
|
aeccf2b1c6 | ||
|
|
d4183a03c0 | ||
|
|
94b53ce7fa | ||
|
|
42ad941ec2 | ||
|
|
6b5321dad8 | ||
|
|
d9bd05c9ec | ||
|
|
d5ed4a38e4 | ||
|
|
e4f9150c9f | ||
|
|
791583e183 | ||
|
|
1ef9346eab | ||
|
|
ba8999914f | ||
|
|
793ed4f0a7 | ||
|
|
eb0e7e2f5f | ||
|
|
45b1c55b67 | ||
|
|
c0ee80629d | ||
|
|
7280c4b2f7 | ||
|
|
21a55b95d9 | ||
|
|
e94cdaec46 | ||
|
|
b1ca073276 | ||
|
|
77bf441e62 | ||
|
|
6e7512c13e | ||
|
|
a65009dfb0 | ||
|
|
5cebddb0ab | ||
|
|
e830a6b180 | ||
|
|
b4b813fe5e | ||
|
|
8fa49137b1 | ||
|
|
de239578cc | ||
|
|
a39419288c | ||
|
|
47c5187ad9 | ||
|
|
b04cb343dd | ||
|
|
a31bdb66c8 | ||
|
|
1ba5011bfa | ||
|
|
e793e7793b | ||
|
|
3066bf84d5 | ||
|
|
efdd5a824b | ||
|
|
12532dee28 | ||
|
|
4175a582b8 | ||
|
|
78a3ff177f | ||
|
|
6792ed4f94 | ||
|
|
8dcd0451ba | ||
|
|
d0d35a7938 | ||
|
|
aabd988a77 | ||
|
|
d171cea627 | ||
|
|
26730efc1b | ||
|
|
ca95b9c14c | ||
|
|
5ea140db98 | ||
|
|
8fded88813 | ||
|
|
1a9239a358 | ||
|
|
cc88738e3e | ||
|
|
0436a3705c | ||
|
|
72979a9743 | ||
|
|
f3ceb9034e | ||
|
|
c55d6966cd | ||
|
|
739b5b5ad6 | ||
|
|
0ad769e08e | ||
|
|
384b8fd489 | ||
|
|
978f41a4d9 | ||
|
|
fda77b49cd | ||
|
|
e8fffa47a0 | ||
|
|
6cb4f7ac8a | ||
|
|
26f3742b31 | ||
|
|
c8216b0acc | ||
|
|
5b9309a311 | ||
|
|
0e50a8a9e5 | ||
|
|
52c1708dd2 | ||
|
|
2e2d3e173e | ||
|
|
fe6e1edecc | ||
|
|
a6e08a1865 | ||
|
|
02f2bf1bc1 | ||
|
|
43d356967c | ||
|
|
05b7234748 | ||
|
|
0f89e24377 | ||
|
|
c34cc4638c | ||
|
|
9f7b95746d | ||
|
|
0ec440c388 | ||
|
|
19526dd92d | ||
|
|
814aa92e19 | ||
|
|
038c230427 | ||
|
|
b725d919bb | ||
|
|
ceb3bbc5c3 | ||
|
|
7cb50030f9 | ||
|
|
c1a9c798ae | ||
|
|
50f81cc889 | ||
|
|
4fc763c9aa | ||
|
|
8318a4bd84 | ||
|
|
e08e9c4d13 | ||
|
|
312fc1df9a | ||
|
|
9ba6e4d0af | ||
|
|
29c93f46a0 | ||
|
|
da423b7464 | ||
|
|
5c6e0701d9 | ||
|
|
a4bd015836 | ||
|
|
1cee1c24ec | ||
|
|
c1cdb28bb5 | ||
|
|
282f6d4855 | ||
|
|
334be441f8 | ||
|
|
7128326ab9 | ||
|
|
0220257efa | ||
|
|
af6100dfe4 | ||
|
|
1d74001281 | ||
|
|
a89aafb310 | ||
|
|
7b0be25f6e | ||
|
|
883580d465 | ||
|
|
1000841f69 | ||
|
|
add4b8aa83 | ||
|
|
529788d2e5 | ||
|
|
90665c615f | ||
|
|
a2bf477481 | ||
|
|
31bc5ec6f9 | ||
|
|
80ce6fe21f | ||
|
|
2e0e125913 | ||
|
|
06cf6ce752 | ||
|
|
a0ac0dfcfa | ||
|
|
d2bfcefb89 | ||
|
|
95d5d6c4b0 | ||
|
|
a5f0c2f943 | ||
|
|
4610686a70 | ||
|
|
e321cbdf96 | ||
|
|
8a0d217977 | ||
|
|
1e4570bd79 | ||
|
|
076dab924f | ||
|
|
5a80e65d3b | ||
|
|
e9628afaf8 | ||
|
|
1649da70a8 | ||
|
|
72775a80bf | ||
|
|
6693a1e0ba | ||
|
|
b543d9fc1d | ||
|
|
a159ef28c3 | ||
|
|
b2bd31a166 | ||
|
|
8487661bc8 | ||
|
|
6c93cc20df | ||
|
|
7b1e28c2cf | ||
|
|
77762734d7 | ||
|
|
056d734c4a | ||
|
|
a2eaa3ed7f | ||
|
|
b79fbc7653 | ||
|
|
363b220613 | ||
|
|
d76411e66c | ||
|
|
c2e5499aef | ||
|
|
b5a71ed7b3 | ||
|
|
4183e29249 | ||
|
|
8167907d91 | ||
|
|
ca393267f6 | ||
|
|
38d855684b | ||
|
|
964ddc2572 | ||
|
|
7db9599511 | ||
|
|
fff9fb00f8 | ||
|
|
edad7d9ec9 | ||
|
|
3debd47064 | ||
|
|
1e33a8bb22 | ||
|
|
153b1e0d83 | ||
|
|
c752835d2c | ||
|
|
de08862a88 | ||
|
|
655d1722c1 | ||
|
|
c11519c95e | ||
|
|
ae409c2cd1 | ||
|
|
0d3dde7df3 | ||
|
|
cbd99f833a | ||
|
|
0486d049b0 | ||
|
|
aa01acd76a | ||
|
|
6da725350a | ||
|
|
6f3be39cb9 | ||
|
|
5c15a3a4ff | ||
|
|
8c763d5379 | ||
|
|
65af4267f0 | ||
|
|
202f6e4728 | ||
|
|
2c5f22047a | ||
|
|
107f6706d3 | ||
|
|
b089bbca37 | ||
|
|
4b81b065aa | ||
|
|
eb02d2a7a7 | ||
|
|
fdd0145975 | ||
|
|
bbac3daf01 | ||
|
|
0b50593acd | ||
|
|
e5ddae585c | ||
|
|
8509ccba30 | ||
|
|
da1b9e9e90 | ||
|
|
47e6e70272 | ||
|
|
0ff7e49e4d | ||
|
|
5e794b73ba | ||
|
|
ea04cc554f | ||
|
|
9fae88934d | ||
|
|
b53a2f1def | ||
|
|
70e72f5790 | ||
|
|
f147e66953 | ||
|
|
965f8efd80 | ||
|
|
574a129772 | ||
|
|
4749769dd4 | ||
|
|
3b9d841014 | ||
|
|
7c83e30e9f | ||
|
|
65fbf13afe | ||
|
|
ec92f93d22 | ||
|
|
0e50cc9c47 | ||
|
|
9ff3227cf4 | ||
|
|
5e6ca8b22c | ||
|
|
a6788c6dd3 | ||
|
|
b5d4b31301 | ||
|
|
f71096f8b0 | ||
|
|
2d8b7efc00 | ||
|
|
ca58c81bce | ||
|
|
2fa7272762 | ||
|
|
e8fd452b8f | ||
|
|
db2081f14d | ||
|
|
4572cb83f0 | ||
|
|
74ffc56d6c | ||
|
|
96f40b7ddc | ||
|
|
509b4c8866 | ||
|
|
c8e58c08a0 | ||
|
|
c6642c4fa3 | ||
|
|
91cea50f02 | ||
|
|
be588e2fa3 | ||
|
|
4a6e7fccec | ||
|
|
63fff56a60 | ||
|
|
b81f3f423c | ||
|
|
2e473a62f4 | ||
|
|
edcffb9d9f | ||
|
|
a0e9e2ead3 | ||
|
|
dada03905f | ||
|
|
0c5d47e3d1 | ||
|
|
7f7b35f370 | ||
|
|
401704712e | ||
|
|
4b198ef1e4 | ||
|
|
182550ce15 | ||
|
|
64aed56f7c | ||
|
|
d2f93f8562 | ||
|
|
3cd438bb5d | ||
|
|
ec114b3f6a | ||
|
|
c3ba8a2231 | ||
|
|
36cbca4684 | ||
|
|
025e3798a7 | ||
|
|
e344bd4258 | ||
|
|
8c7c7e20a0 | ||
|
|
b0f61e6929 | ||
|
|
a2b92f1296 | ||
|
|
4c18b747b1 | ||
|
|
2e935a6378 | ||
|
|
2f6905cf35 | ||
|
|
3f8ac1e8d0 | ||
|
|
8bc71fb1b3 | ||
|
|
0440324432 | ||
|
|
aa7f0bace9 | ||
|
|
b62bc44564 | ||
|
|
1a88cefd52 | ||
|
|
981721ae85 | ||
|
|
9311f80455 | ||
|
|
fe92ac34f0 | ||
|
|
677c9bd801 | ||
|
|
51bb9cf7cd | ||
|
|
13d594ca87 | ||
|
|
1a1c662364 | ||
|
|
6de3077afa | ||
|
|
b5b3e1b1f2 | ||
|
|
c33545acdf | ||
|
|
55f38865e3 | ||
|
|
306a9c217a | ||
|
|
1b98626a61 | ||
|
|
41d900ff51 | ||
|
|
3f1234373d | ||
|
|
70a09264a8 | ||
|
|
6641f5425b | ||
|
|
acaa49fec5 | ||
|
|
4e8695e7a4 | ||
|
|
79de6f1714 | ||
|
|
b3fe538219 | ||
|
|
b7edf521b6 | ||
|
|
e8e87cc6cb | ||
|
|
655e2fd2ca | ||
|
|
d85cbd8051 | ||
|
|
215f807483 | ||
|
|
f71d922198 | ||
|
|
bb9e7cac07 | ||
|
|
73ff3642fc | ||
|
|
9f981a3e52 | ||
|
|
a059942bb2 | ||
|
|
08ed3ca447 | ||
|
|
518117b25a | ||
|
|
bc068f9913 | ||
|
|
68c782f0b9 | ||
|
|
da019e729d | ||
|
|
dc845b766e | ||
|
|
f1379af92c | ||
|
|
551c25a64c | ||
|
|
6a2b802196 | ||
|
|
989915ddbe | ||
|
|
309f0351fa | ||
|
|
81cdcad72e | ||
|
|
f7a2c17415 | ||
|
|
727fa3c183 | ||
|
|
697b5fac65 | ||
|
|
b5c69b2946 | ||
|
|
695c18439d | ||
|
|
18fd36d2d7 | ||
|
|
71fc901798 | ||
|
|
d7cac3e09a | ||
|
|
d646c5e4b5 | ||
|
|
635d606112 | ||
|
|
bc24110c9f | ||
|
|
ca46e7482f | ||
|
|
81425b458e | ||
|
|
b7472f722e | ||
|
|
ed283afe2c | ||
|
|
df43083101 | ||
|
|
f8331bc4d8 | ||
|
|
28752e2630 | ||
|
|
19866c5638 | ||
|
|
a001fcf24f | ||
|
|
dc583cb8e2 | ||
|
|
4aa19e49d5 | ||
|
|
b22470ac79 | ||
|
|
a581495c7e | ||
|
|
829016a1c4 | ||
|
|
72f57d292b | ||
|
|
2051197c65 | ||
|
|
c138c39c06 | ||
|
|
bb382459eb | ||
|
|
17e6838422 | ||
|
|
7ef50f7bb4 | ||
|
|
28246244cd | ||
|
|
f04b295989 | ||
|
|
27123f2a64 | ||
|
|
8ba20218c6 | ||
|
|
49b63d2208 | ||
|
|
2a0e6ce1aa | ||
|
|
6d89ea5a71 | ||
|
|
969ba38ffe | ||
|
|
f022d2be64 | ||
|
|
d8f5851e0c | ||
|
|
5d28904bdf | ||
|
|
6dc5916f2b | ||
|
|
b494892d62 | ||
|
|
7d612df951 | ||
|
|
3305250482 | ||
|
|
0514e72d47 | ||
|
|
173b4d7306 | ||
|
|
76cb09b3b5 | ||
|
|
54776c45ea | ||
|
|
59ea1f2dd6 | ||
|
|
b14cd26e4e | ||
|
|
bb742463e9 | ||
|
|
a9f36c6aef | ||
|
|
7128f237da | ||
|
|
a0328aab35 | ||
|
|
e0fa8c9285 | ||
|
|
2c1ce66011 | ||
|
|
222a0e5f77 | ||
|
|
07e20fb670 | ||
|
|
b595a0da0f | ||
|
|
eb0e334437 | ||
|
|
e497414cb7 | ||
|
|
e0749bb791 | ||
|
|
909778c5b4 | ||
|
|
3d9f8355d2 | ||
|
|
4aa1388b34 | ||
|
|
b3c757c37b | ||
|
|
b727220775 | ||
|
|
1101a7a986 | ||
|
|
18d38a9974 | ||
|
|
bb3d3657ed | ||
|
|
d647a62e82 | ||
|
|
0f03e0484c | ||
|
|
73af509885 | ||
|
|
5ebab472b8 | ||
|
|
d05f369a94 | ||
|
|
5e76ab3b84 | ||
|
|
a408b8918c | ||
|
|
43c6b52d0b | ||
|
|
a6f7fd623c | ||
|
|
4204262236 | ||
|
|
8a67521511 | ||
|
|
55ddafea4b | ||
|
|
357b11eb25 | ||
|
|
85500f0e9d | ||
|
|
3e6967002b | ||
|
|
d84715ad27 | ||
|
|
69c493b9d6 | ||
|
|
908239bf13 | ||
|
|
ea65296ab7 | ||
|
|
dc1c8f42c0 | ||
|
|
2fd8c98147 | ||
|
|
c88f1a7b1c | ||
|
|
7e8cd719fd | ||
|
|
806561b95a | ||
|
|
8caba8c339 | ||
|
|
63ca044586 | ||
|
|
3d38495f92 | ||
|
|
acfd5d2484 | ||
|
|
aee942468e | ||
|
|
d026ca888f | ||
|
|
ab902cbe9e | ||
|
|
13044763cb | ||
|
|
cb43fed9d3 | ||
|
|
60551c8739 | ||
|
|
b2bf6eb0f7 | ||
|
|
4e26f09109 | ||
|
|
51dba221c4 | ||
|
|
b5377a961f | ||
|
|
da880bd76c | ||
|
|
5a64eadb5c | ||
|
|
50a7015bc5 | ||
|
|
fd163f8f66 | ||
|
|
2852562a03 | ||
|
|
c024d7e826 | ||
|
|
7dabb3c647 | ||
|
|
79c43fe7b1 | ||
|
|
69a4e2b52e | ||
|
|
da54222bb1 | ||
|
|
57f8587a43 | ||
|
|
28a396470b | ||
|
|
3da20f2d89 | ||
|
|
db9bfb00a3 | ||
|
|
5085aa500c | ||
|
|
00dc5f48b1 | ||
|
|
6375faa758 | ||
|
|
8e63452e84 | ||
|
|
06e06b81e9 | ||
|
|
c76a9ace24 | ||
|
|
8fd755c5e6 | ||
|
|
2cb92d817a | ||
|
|
25e9a99799 | ||
|
|
f910dcf1e0 | ||
|
|
f2ef0e15d3 | ||
|
|
23f46438a2 | ||
|
|
5e79a13708 | ||
|
|
5d2fc72883 | ||
|
|
3c59a57ab0 | ||
|
|
c24a40fd9f | ||
|
|
f5822cf2c8 | ||
|
|
4bdc8f126d | ||
|
|
764ef76e1a | ||
|
|
3699923938 | ||
|
|
4378c826f0 | ||
|
|
717ddba8d2 | ||
|
|
9871421632 | ||
|
|
7cdd8656ef | ||
|
|
2d007c189f | ||
|
|
9fda19d4c0 | ||
|
|
110298f280 | ||
|
|
422324b6d7 | ||
|
|
48863c1b64 | ||
|
|
de3a74bbe8 | ||
|
|
3b593103ba | ||
|
|
dd587350ea | ||
|
|
52d38eda3a | ||
|
|
779d6b37a5 | ||
|
|
19c4c3b50e | ||
|
|
c487fb12ec | ||
|
|
8b5437c2c7 | ||
|
|
73b4227310 | ||
|
|
30a55d401f | ||
|
|
0aeb407a01 | ||
|
|
53f1efa88b | ||
|
|
069929ce24 | ||
|
|
c21cbcdcd3 | ||
|
|
8e0877659f | ||
|
|
83ab8e8003 | ||
|
|
a18ace433a | ||
|
|
58b5c44157 | ||
|
|
5fefdfa33b | ||
|
|
fb591429d6 | ||
|
|
e5427858e0 | ||
|
|
3d2ce1f4bb | ||
|
|
95746e7450 | ||
|
|
5394bdc535 | ||
|
|
8f16aa7ee9 | ||
|
|
0dc06a1733 | ||
|
|
4e40ed3be4 | ||
|
|
21d503a8cd | ||
|
|
50f6de7809 | ||
|
|
c09568e406 | ||
|
|
929db5c1a4 | ||
|
|
6799bdbb03 | ||
|
|
6ca8ad2385 | ||
|
|
efdebeca54 | ||
|
|
677d44442b | ||
|
|
6130929985 | ||
|
|
4c73453b4c | ||
|
|
8aebd441a1 | ||
|
|
2c0650614f | ||
|
|
3f439bacb2 | ||
|
|
11bf0d2998 | ||
|
|
40b6c6022a | ||
|
|
69388689ac | ||
|
|
5a24d9155b | ||
|
|
1760efb477 | ||
|
|
348480ed68 | ||
|
|
c29d0a5a4c | ||
|
|
89c7095843 | ||
|
|
808d7ab017 | ||
|
|
f02a37b939 | ||
|
|
69012e5ecd | ||
|
|
5067ab2bb2 | ||
|
|
5506dcc3f3 | ||
|
|
fee99dd17e | ||
|
|
4ffb69ea31 | ||
|
|
3905d5b976 | ||
|
|
ea79469abd | ||
|
|
a241b933ca | ||
|
|
22966e648d | ||
|
|
d7205344eb | ||
|
|
2b4a01df06 | ||
|
|
53adb6fa54 | ||
|
|
25cdc00404 | ||
|
|
916ff0cbb2 | ||
|
|
50d7619dde | ||
|
|
28c2af4266 | ||
|
|
652b2e99d2 | ||
|
|
c5ef7bf46c | ||
|
|
470c1317ed | ||
|
|
4e704770cb | ||
|
|
e26873934b | ||
|
|
7431db2a08 | ||
|
|
b352373a52 | ||
|
|
d21cba4669 | ||
|
|
4fdb89ce62 | ||
|
|
8eaf14d932 | ||
|
|
569fa06e18 | ||
|
|
40eb0c81b8 | ||
|
|
ed243df4f3 | ||
|
|
4bd3fd357f | ||
|
|
6cc89f3e7c | ||
|
|
a890258cf5 | ||
|
|
1cb74aeb9a | ||
|
|
0e0733dab0 | ||
|
|
32608ea45b | ||
|
|
94a0a3902c | ||
|
|
176956a1f8 | ||
|
|
2a2fa3bf1d | ||
|
|
cca626449d | ||
|
|
a17a1e9576 | ||
|
|
30c622c085 | ||
|
|
db521dd21c | ||
|
|
ccc0b51a99 | ||
|
|
ecfe88faa6 | ||
|
|
052811049e | ||
|
|
0741ce0ce7 | ||
|
|
b985833aaa | ||
|
|
1b490510c7 | ||
|
|
5899a59e06 | ||
|
|
686c53d919 | ||
|
|
233a865c78 | ||
|
|
0dbe9b59c2 | ||
|
|
6f760426c7 | ||
|
|
af4373ce50 | ||
|
|
82cecdaf7d | ||
|
|
616a4635d1 | ||
|
|
a768b039a8 | ||
|
|
e5e555b981 | ||
|
|
ff01276869 | ||
|
|
5023fafc19 | ||
|
|
2ac997610d | ||
|
|
8695e89792 | ||
|
|
5ba993cd6f | ||
|
|
6d3e930440 | ||
|
|
f238049750 | ||
|
|
600f5987cd | ||
|
|
38a22c5298 | ||
|
|
5346abaadf | ||
|
|
bb8d9441f4 | ||
|
|
848e4ff8a6 | ||
|
|
598f3db06a | ||
|
|
f54146ada4 | ||
|
|
ffb8f0e8d3 | ||
|
|
6c0864c8b9 | ||
|
|
ec14efb789 | ||
|
|
ead88f9fa6 | ||
|
|
99b43bf577 | ||
|
|
792707a6e3 | ||
|
|
4f71065d67 | ||
|
|
781bbb3d26 | ||
|
|
87c5164367 | ||
|
|
afd7aab37d | ||
|
|
42b874413d | ||
|
|
9364ecccd2 | ||
|
|
b8d09ab660 | ||
|
|
f64fdd2b26 | ||
|
|
ccca2f1434 | ||
|
|
3530e139d1 | ||
|
|
a6ae580b9f | ||
|
|
3a8bf5dfa1 | ||
|
|
00adaca32e | ||
|
|
bc6e9d1d84 | ||
|
|
ad830dc56e | ||
|
|
ebaa42f311 | ||
|
|
4c611530f3 | ||
|
|
82ba2cd16a | ||
|
|
635d5e05ce | ||
|
|
53b36f2597 | ||
|
|
0c07d4bec6 | ||
|
|
04b76ddee1 | ||
|
|
cf3810a1b8 | ||
|
|
af536b3423 | ||
|
|
09ca32f33d | ||
|
|
df808187e2 | ||
|
|
e615ffcf3d | ||
|
|
6e11b36401 | ||
|
|
af93c2aca9 | ||
|
|
e24a535a93 | ||
|
|
2f836426d6 | ||
|
|
2a7ccb952d | ||
|
|
0e252d489a | ||
|
|
af0edf3002 | ||
|
|
01b88950bf | ||
|
|
622af4e7e9 | ||
|
|
930931a846 | ||
|
|
2da6a33a62 | ||
|
|
a95877b9e4 | ||
|
|
3738b5f8f0 | ||
|
|
4df616e4c0 | ||
|
|
1d5e050de6 | ||
|
|
ef916fc93c | ||
|
|
18b6b87e6b | ||
|
|
dccd347432 | ||
|
|
ec1dee8871 | ||
|
|
b8c9a98ba2 | ||
|
|
8d7c779439 | ||
|
|
a6d68ddd5a | ||
|
|
96e6ff0fbf | ||
|
|
4f2a14c9ee | ||
|
|
774f93f962 | ||
|
|
27c9523bd7 | ||
|
|
808dabf600 | ||
|
|
344defca8e | ||
|
|
65fc029218 | ||
|
|
c3b106e359 | ||
|
|
62f71df28c | ||
|
|
0fb9e77c3c | ||
|
|
9e0c38169f | ||
|
|
a7ace8a8c8 | ||
|
|
64668e0e03 | ||
|
|
4626c2176f | ||
|
|
2bc7eb165e | ||
|
|
0521cf0d18 | ||
|
|
6e7805d58f | ||
|
|
e3a608fe0e | ||
|
|
a6b929c207 | ||
|
|
869be0cb95 | ||
|
|
93e1b7acb9 | ||
|
|
81dae22936 | ||
|
|
823b195cb1 | ||
|
|
00bc17c57a | ||
|
|
8ea6893fc3 | ||
|
|
0ed94676ed | ||
|
|
0d343c3bab | ||
|
|
0690c0c53c | ||
|
|
f88b5761ba | ||
|
|
22cb33e49e | ||
|
|
895e70555d | ||
|
|
5805d5c798 | ||
|
|
fbd8a12f3a | ||
|
|
5f916efb13 | ||
|
|
3f1d84343a | ||
|
|
306c2ffd10 | ||
|
|
208d8a11ff | ||
|
|
d42a105687 | ||
|
|
8436455936 | ||
|
|
323b4d6f21 | ||
|
|
b3a1a979eb | ||
|
|
636f14a06d | ||
|
|
03cb88be10 | ||
|
|
3b68eca212 | ||
|
|
37798d93ba | ||
|
|
712dcf5782 | ||
|
|
5a9f1385a2 | ||
|
|
0999ab804a | ||
|
|
4f63e32df3 | ||
|
|
6b4e60e42e | ||
|
|
387b6da4d5 | ||
|
|
e72479c046 | ||
|
|
5aec508616 | ||
|
|
d9c5c053cf | ||
|
|
5fcb07487e | ||
|
|
78e772dad9 | ||
|
|
878395e164 | ||
|
|
6ec60c9150 | ||
|
|
b748e34917 | ||
|
|
b5f20c0ec8 | ||
|
|
52efacacd7 | ||
|
|
d24e1576d3 | ||
|
|
c991eead89 | ||
|
|
0404ea6109 | ||
|
|
9255f2bb2b | ||
|
|
e02de6de5a | ||
|
|
768016a897 | ||
|
|
3dc1553429 | ||
|
|
5320f43491 | ||
|
|
77e76dd8a2 | ||
|
|
071317e168 | ||
|
|
2bc8092cca | ||
|
|
381ab4befe | ||
|
|
81891cfe09 | ||
|
|
9fb5ac65d1 | ||
|
|
02fe5a4fb3 | ||
|
|
f8d1fcf4e2 | ||
|
|
312cb23615 | ||
|
|
e28483d1ae | ||
|
|
e98003eb09 | ||
|
|
0243e7a633 | ||
|
|
f938531e21 | ||
|
|
96aaefd3e2 | ||
|
|
7244d63e1e | ||
|
|
9950604867 | ||
|
|
edcfea5701 | ||
|
|
575c1e2118 | ||
|
|
f303b9e443 | ||
|
|
78aff2b9dc | ||
|
|
b51ced8cfb | ||
|
|
1a36b74557 | ||
|
|
e14fedf59e | ||
|
|
5567134a56 | ||
|
|
4298b46130 | ||
|
|
0aa74692a8 | ||
|
|
3f03712e24 | ||
|
|
cbda4614a9 | ||
|
|
c86d2eded5 | ||
|
|
5d96f789fe | ||
|
|
b50564f741 | ||
|
|
654978dd64 | ||
|
|
f01b2f8754 | ||
|
|
e4e74376fc | ||
|
|
5ba43c1b19 | ||
|
|
e8eff51d84 | ||
|
|
f9cc88cbb0 | ||
|
|
d403f44256 | ||
|
|
8e5ed60c79 | ||
|
|
6f6b72e7aa | ||
|
|
e5c4743374 | ||
|
|
4d9c5bdb8d | ||
|
|
beb777e3cd | ||
|
|
6316458613 | ||
|
|
4b1443ec93 | ||
|
|
c911977b5e | ||
|
|
4cd03f2198 | ||
|
|
58bd223a80 | ||
|
|
fb84b43d69 | ||
|
|
f46daf0f54 | ||
|
|
314a1e0e8c | ||
|
|
9c0406ec9d | ||
|
|
94a0864556 | ||
|
|
ee50994b39 | ||
|
|
a38a989fe7 | ||
|
|
2167ddf9d9 | ||
|
|
c2fb18ab53 | ||
|
|
7ab5c7311c | ||
|
|
101a1b7392 | ||
|
|
f9b1e85c8f | ||
|
|
340a35918c | ||
|
|
1ccf3a4256 | ||
|
|
23e553c88e | ||
|
|
d9dc37c994 | ||
|
|
361244385f | ||
|
|
60754012c2 | ||
|
|
ca0caebe84 | ||
|
|
8d37c5ff06 | ||
|
|
127bbcb485 | ||
|
|
42ef951b82 | ||
|
|
d8597009a8 | ||
|
|
b1ab7e1cd0 | ||
|
|
777c0cc69e | ||
|
|
46bf3d7391 | ||
|
|
183b59305a | ||
|
|
ef5cf14b2b | ||
|
|
a9ff6135b3 | ||
|
|
89b5877443 | ||
|
|
3d5765796e | ||
|
|
c2933ba95c | ||
|
|
73b4787a55 | ||
|
|
648286a923 | ||
|
|
cd94c73d93 | ||
|
|
d831b61c02 | ||
|
|
b8ad456ed3 | ||
|
|
c2fe0d6ed1 | ||
|
|
d649a3b1a7 | ||
|
|
290912e7cd | ||
|
|
2402d0aa6f | ||
|
|
31338e43d6 | ||
|
|
7d1d6ac829 | ||
|
|
a293e7dfea | ||
|
|
5ba455fe71 | ||
|
|
3bd6b0ccea | ||
|
|
fd3a066aee | ||
|
|
ce03fb59c8 | ||
|
|
a94c5ae7af | ||
|
|
e66d666d4d | ||
|
|
826777b7ee | ||
|
|
c49454fc25 | ||
|
|
2c55701cbf | ||
|
|
be3c1c85aa | ||
|
|
aa4bc45641 | ||
|
|
49c8afb72a | ||
|
|
10c0117402 | ||
|
|
9c4f7b7562 | ||
|
|
25cb46525a | ||
|
|
1364b39f65 | ||
|
|
e3c333dd22 | ||
|
|
49ba771b26 | ||
|
|
fba5bc6820 | ||
|
|
e9fc57022e | ||
|
|
835020229c | ||
|
|
4972dd1c9f | ||
|
|
1d82e882ed | ||
|
|
0186f176d0 | ||
|
|
9037166d92 | ||
|
|
85fb98b557 | ||
|
|
0108e51636 | ||
|
|
828cd07df0 | ||
|
|
e86899c943 | ||
|
|
2bf80dfa6b | ||
|
|
a88332d3bc | ||
|
|
8c28f0c6e3 | ||
|
|
3f33bab296 | ||
|
|
51ce6d1038 | ||
|
|
d908f22a17 | ||
|
|
c527d19117 | ||
|
|
4294b18bcb | ||
|
|
bbfc9a0a6f | ||
|
|
bfb630d317 | ||
|
|
31c9b22b9b | ||
|
|
19efd766fc | ||
|
|
2bfd5d138f | ||
|
|
3f4cd67dae | ||
|
|
eddbd2b14f | ||
|
|
69ce929c5e | ||
|
|
0ed1a81c29 | ||
|
|
f85fc46fb7 | ||
|
|
5a817db069 | ||
|
|
e9ab9a71a8 | ||
|
|
577669b21f | ||
|
|
c12dbf3f8a | ||
|
|
d4738934f8 | ||
|
|
113078af90 | ||
|
|
a1e9c44697 | ||
|
|
49f1f7020f | ||
|
|
a2fd070c86 | ||
|
|
e79b110429 | ||
|
|
2ffbd7beba | ||
|
|
afa11f85e2 | ||
|
|
70c1a2604f | ||
|
|
1541cdb78d | ||
|
|
3f86698615 | ||
|
|
185be81e73 | ||
|
|
a8000fbf14 | ||
|
|
2b7292adb8 | ||
|
|
c31a2f5a42 | ||
|
|
fbe2ed1a71 | ||
|
|
1253079968 | ||
|
|
ccdafcf85d | ||
|
|
ef9022a746 | ||
|
|
e33f49e097 | ||
|
|
182546ee10 | ||
|
|
c958935f40 | ||
|
|
355206e0cf | ||
|
|
d58a3e0fe7 | ||
|
|
348da38879 | ||
|
|
fb2fe05409 | ||
|
|
d32e777426 | ||
|
|
831990949f | ||
|
|
9ee8ab73ec | ||
|
|
45b26030cc | ||
|
|
5cad575c2e | ||
|
|
c8415e3079 | ||
|
|
174e640c45 | ||
|
|
f38a252295 | ||
|
|
7bad131542 | ||
|
|
56286e0123 | ||
|
|
49f1e2dcde | ||
|
|
e6b17d536b | ||
|
|
d28299f699 | ||
|
|
046ef4d72d | ||
|
|
7a6384bd22 | ||
|
|
14eddac6f7 | ||
|
|
045c84512f | ||
|
|
b6d6993c9f | ||
|
|
c5ac9f6f08 | ||
|
|
21181370e7 | ||
|
|
b92a3161b5 | ||
|
|
651c7410ac | ||
|
|
dd8c910597 | ||
|
|
2670ba52c1 | ||
|
|
3fc724b7ee | ||
|
|
0df12a34cb | ||
|
|
99fd4b7806 | ||
|
|
bdaff7b781 | ||
|
|
73e2793da6 | ||
|
|
3c564add0e | ||
|
|
1b7360f8be | ||
|
|
19dde3cbc4 | ||
|
|
d8e2a5ba28 | ||
|
|
23c1ee9dc6 | ||
|
|
8ce52b7028 | ||
|
|
f03584a057 | ||
|
|
cd894e415d | ||
|
|
5c6c96b6c0 | ||
|
|
9c6bcb2409 | ||
|
|
8d38f73f52 | ||
|
|
6a54d24634 | ||
|
|
4a8f0aac61 | ||
|
|
6159f1e998 | ||
|
|
c2bb1407a9 | ||
|
|
6ee6e4a4ba | ||
|
|
45075d5b27 | ||
|
|
64c8f29c47 | ||
|
|
009499cdf6 | ||
|
|
3c78d6b695 | ||
|
|
21be245c5c | ||
|
|
522fc832db | ||
|
|
cdc4ee6991 | ||
|
|
1f942491ac | ||
|
|
c2ac745d72 | ||
|
|
e62b0155d4 | ||
|
|
7ae6d0a348 | ||
|
|
2e6cc73666 | ||
|
|
d175802bec | ||
|
|
397362caa5 | ||
|
|
c4a4aec221 | ||
|
|
7619503a2b | ||
|
|
d4f1097eba | ||
|
|
9cf69def7b | ||
|
|
b31c5fdd1f | ||
|
|
47ddca0506 | ||
|
|
ebbc3fed86 | ||
|
|
7e56858bc6 | ||
|
|
a2b62a8b6a | ||
|
|
1e471551d4 | ||
|
|
e058b6e32b | ||
|
|
86de28245d | ||
|
|
33231959b2 | ||
|
|
0c17892f03 | ||
|
|
7ee80c7d48 | ||
|
|
0a47ae5b18 | ||
|
|
30fba90e9f | ||
|
|
738d62757c | ||
|
|
579a9edca1 | ||
|
|
9098b5b3b3 | ||
|
|
08519396a0 | ||
|
|
68a787d125 | ||
|
|
c3a71ab95e | ||
|
|
f16c6363ab | ||
|
|
bdef7a5118 | ||
|
|
bb805345b1 | ||
|
|
3dc04293eb | ||
|
|
92ddc250d0 | ||
|
|
41b88b036e | ||
|
|
b26923e504 | ||
|
|
ea66bd2e67 | ||
|
|
f29bdee010 | ||
|
|
98bc14882b | ||
|
|
9f6a45041d | ||
|
|
e34aca68aa | ||
|
|
60f54fa047 | ||
|
|
41ddc451de | ||
|
|
c2b3e4dbaf | ||
|
|
5997ddca02 | ||
|
|
fe561f39c2 | ||
|
|
58c74e839c | ||
|
|
960c936f8d | ||
|
|
20303a9416 | ||
|
|
d5efe3f748 | ||
|
|
2ef9d3d56e | ||
|
|
21e6a17d1c | ||
|
|
e716bbbc01 | ||
|
|
0239c2f60b | ||
|
|
5f63d4de38 | ||
|
|
e6d73971e9 | ||
|
|
a9a5f91c90 | ||
|
|
853fe8644c | ||
|
|
24fda725a2 | ||
|
|
7ab1426a2c | ||
|
|
471005b5b1 | ||
|
|
deb630795d | ||
|
|
513df2beac | ||
|
|
a11e1d464b | ||
|
|
832b1163e0 | ||
|
|
123dd3aacc | ||
|
|
93840e30f0 | ||
|
|
fe0e01b8fe | ||
|
|
b0370139ec | ||
|
|
f9b8717582 | ||
|
|
bff99da585 | ||
|
|
b64a9a51f8 | ||
|
|
6b558c5940 | ||
|
|
23919d8083 | ||
|
|
7bb5a1ebe3 | ||
|
|
e55ff791fe | ||
|
|
80f02e5377 | ||
|
|
123ed256b1 | ||
|
|
4059e0630a | ||
|
|
79eee0e2c7 | ||
|
|
27fed7860d | ||
|
|
ff2b9de93e | ||
|
|
efdece613a | ||
|
|
79b4415a44 | ||
|
|
c7cb771992 | ||
|
|
9555b4eecb | ||
|
|
e00cb6cc6a | ||
|
|
9ccbe10642 | ||
|
|
889fc101a8 | ||
|
|
58f86743eb | ||
|
|
aa15ff40e1 | ||
|
|
a061ab9b8b | ||
|
|
40b7266c22 | ||
|
|
ea2a411a2e | ||
|
|
f27d49f5d6 | ||
|
|
b7408f15fb | ||
|
|
d23eab0530 | ||
|
|
a7d7f1523f | ||
|
|
7b318c9ce4 | ||
|
|
28ab12c21b | ||
|
|
72408bf45c | ||
|
|
baf3b06060 | ||
|
|
6e983c8735 | ||
|
|
b43d0453e1 | ||
|
|
c684db3000 | ||
|
|
ef962393c4 | ||
|
|
2b8862e4a5 | ||
|
|
2f7b6e3d55 | ||
|
|
f2997102c7 | ||
|
|
6bc0b77ad3 | ||
|
|
b3b552235c | ||
|
|
0158ff2074 | ||
|
|
0b7b63a3a9 | ||
|
|
2dda954806 | ||
|
|
8df4bb0781 | ||
|
|
36c77034a4 | ||
|
|
20e6baee0b | ||
|
|
1268344d04 | ||
|
|
047f14a288 | ||
|
|
fbb8f48e49 | ||
|
|
bc3a55eded | ||
|
|
070d73a5a1 | ||
|
|
45ec212b78 | ||
|
|
29d01e698b | ||
|
|
6493394256 | ||
|
|
c590b7fb24 | ||
|
|
5dd4701c4c | ||
|
|
ab53f17a7e | ||
|
|
33b4905ae2 | ||
|
|
a01f73cde4 | ||
|
|
6cd43aa304 | ||
|
|
3eb35c479e | ||
|
|
cc55ebb28f | ||
|
|
5b3d5d1e67 | ||
|
|
e534ce37d5 | ||
|
|
87b6fe6aa6 | ||
|
|
4df9ac4632 | ||
|
|
743f449a49 | ||
|
|
5cd4b49fee | ||
|
|
ef19af481b | ||
|
|
707ae090bf | ||
|
|
3e26972a15 | ||
|
|
8bca3d82f5 | ||
|
|
d5e2fc3b05 | ||
|
|
506f7d5824 | ||
|
|
347e4b2023 | ||
|
|
6a3d214e15 | ||
|
|
b40de0e125 | ||
|
|
6f356105cc | ||
|
|
1ae96c71a3 | ||
|
|
f872a14747 | ||
|
|
dc493268f8 | ||
|
|
f63903e3e6 | ||
|
|
727cf7e313 | ||
|
|
d0ed8abab8 | ||
|
|
b65bef17b2 | ||
|
|
c800f3191f | ||
|
|
cac6017392 | ||
|
|
075f8bafa0 | ||
|
|
84b0fc3f69 | ||
|
|
9af7e9d948 | ||
|
|
63a22198aa | ||
|
|
2b6275fe67 | ||
|
|
882a59c1bf | ||
|
|
5f4351d4f1 | ||
|
|
1887a785f5 | ||
|
|
e221c275a2 | ||
|
|
2e272f8e3a | ||
|
|
69ced1089c | ||
|
|
a6b3aab61a | ||
|
|
c06eb1ad3d | ||
|
|
bfddcdd7e2 | ||
|
|
114ed5954e | ||
|
|
2606632533 | ||
|
|
5df00b0c7f | ||
|
|
972187d8ed | ||
|
|
7db67fefa8 | ||
|
|
7635c0c834 | ||
|
|
0534fecc0c | ||
|
|
37a56c56af | ||
|
|
ca51c3b107 | ||
|
|
f1b495dff4 | ||
|
|
b50ed4b99a | ||
|
|
2900351b9a | ||
|
|
69703ed97f | ||
|
|
95d7bc0023 | ||
|
|
4435bb035a | ||
|
|
3391a8ce71 | ||
|
|
7a09d561e9 | ||
|
|
7033b65d33 | ||
|
|
eedd3e2dac | ||
|
|
a64273bd73 | ||
|
|
776d993589 | ||
|
|
29af320092 | ||
|
|
74ed6edd6f | ||
|
|
16a56eb5d0 | ||
|
|
304b75e7d2 | ||
|
|
e47ca842b2 | ||
|
|
41ed873eaf | ||
|
|
c8edd87df8 | ||
|
|
893e0a13bd | ||
|
|
2fac923452 | ||
|
|
f676bd1889 | ||
|
|
03bbba6735 | ||
|
|
353694177e | ||
|
|
4309ae8ce2 | ||
|
|
6a6eac1c3b | ||
|
|
f8c0702432 | ||
|
|
bda3c1f1ac | ||
|
|
0444c28187 | ||
|
|
17a8e06c1d | ||
|
|
4df8f720f5 | ||
|
|
b3a993a2bc | ||
|
|
0b1a11132b | ||
|
|
cbc27d31da | ||
|
|
e6fce32975 | ||
|
|
76fc235cb7 | ||
|
|
0e7c564d14 | ||
|
|
8d11e1075d | ||
|
|
7e167cf0cf | ||
|
|
732ca561a1 | ||
|
|
cbdac759b3 | ||
|
|
68a725d51d | ||
|
|
878f69fd91 | ||
|
|
1353e591b8 | ||
|
|
8dab9407ad | ||
|
|
ef3ffb5f10 | ||
|
|
52b2b66cd7 | ||
|
|
1046c4e991 | ||
|
|
3c0cdc3d0a | ||
|
|
35baba18bf | ||
|
|
bc901f3ff6 | ||
|
|
a49e3312d3 | ||
|
|
cb4f9f8131 | ||
|
|
ccfc05f2b2 | ||
|
|
b9662e39a9 | ||
|
|
e281843760 | ||
|
|
af3575a053 | ||
|
|
1be3b06292 | ||
|
|
b3814e61d1 | ||
|
|
847d8432ff | ||
|
|
febfa8836e | ||
|
|
6039be8685 | ||
|
|
8b156c7d58 | ||
|
|
1a1cbb5404 | ||
|
|
6eb2d9a9d2 | ||
|
|
bbfdc7fad2 | ||
|
|
707f308fac | ||
|
|
e226b20953 | ||
|
|
8f93df533a | ||
|
|
918d5db6a6 | ||
|
|
b3f048bfe6 | ||
|
|
d6d4a0db4c | ||
|
|
b7a09bd3bc | ||
|
|
8d597f9da5 | ||
|
|
31ac6187bc | ||
|
|
097923f5ff | ||
|
|
6014d37bed | ||
|
|
fabe2a9a16 | ||
|
|
fe4955f8fc | ||
|
|
1808d263da | ||
|
|
80efa1ccb8 | ||
|
|
7baed8d430 | ||
|
|
bb06c27359 | ||
|
|
1d6d696cb7 | ||
|
|
ef418b6821 | ||
|
|
c681f1533d | ||
|
|
55c17c7845 | ||
|
|
49b53b7a6a | ||
|
|
6b6e686ee6 | ||
|
|
5cdb13328c | ||
|
|
d7f8476e5b | ||
|
|
9cfb85d1aa | ||
|
|
ec0a6fa1b1 | ||
|
|
e98deab60c | ||
|
|
bb8d9f9ad9 | ||
|
|
4f1106344a | ||
|
|
0ff851f717 | ||
|
|
133a912941 | ||
|
|
2ee64137a7 | ||
|
|
566a5b1fd5 | ||
|
|
0d150cf19b | ||
|
|
62d3053d34 | ||
|
|
35c6e0ec88 | ||
|
|
020cdbb868 | ||
|
|
b359c18360 | ||
|
|
b7b15532f8 | ||
|
|
3158d3da8c | ||
|
|
cadaafb887 | ||
|
|
5c81970558 | ||
|
|
daaee4feb6 | ||
|
|
1f2d5246fe | ||
|
|
47f5e14972 | ||
|
|
ac52515e0d | ||
|
|
4b7315d364 | ||
|
|
3ad811a1d0 | ||
|
|
8d48fcff42 | ||
|
|
819264045b | ||
|
|
fe8f2e2fc5 | ||
|
|
aeb2e9facd | ||
|
|
1c97b52179 | ||
|
|
ea023ebb5c | ||
|
|
57e66f9b66 | ||
|
|
257c0d390b | ||
|
|
358064cd5f | ||
|
|
5538c5704d | ||
|
|
273111775c | ||
|
|
8597070063 | ||
|
|
01c360416f | ||
|
|
19e5e94c64 | ||
|
|
b34999a1a5 | ||
|
|
af3a07c227 | ||
|
|
9753c14b32 | ||
|
|
93b11fb705 | ||
|
|
d11b7e11aa | ||
|
|
1dae7fe036 | ||
|
|
ce73385333 | ||
|
|
7c955cc236 | ||
|
|
72ef666d51 | ||
|
|
fabbeeae13 | ||
|
|
2404002041 | ||
|
|
e5ba2317ac | ||
|
|
d89db756f3 | ||
|
|
d820b886b3 | ||
|
|
521c86d81d | ||
|
|
d65488632a | ||
|
|
aecb033537 | ||
|
|
cceab7d98d | ||
|
|
e9b12da97e | ||
|
|
f15c20a999 | ||
|
|
7b7f241923 | ||
|
|
5eda2d3a23 | ||
|
|
7b4654ce34 | ||
|
|
6e82242a72 | ||
|
|
1fe334e33a | ||
|
|
4beded8a7a | ||
|
|
1ba38a7704 | ||
|
|
55de29e0ac | ||
|
|
0f35dd69f9 | ||
|
|
d12a3dd152 | ||
|
|
75182d094b | ||
|
|
e0000c9ef9 | ||
|
|
dcdc6d1be1 | ||
|
|
07af64feed | ||
|
|
9ce948e238 | ||
|
|
ae842720ee | ||
|
|
37ad1f68b0 | ||
|
|
df249618ab | ||
|
|
2010e02034 | ||
|
|
8aec63d0be | ||
|
|
83d1ced8f5 | ||
|
|
6b76337ec4 | ||
|
|
301d3b2736 | ||
|
|
9f3871eb6d | ||
|
|
94096a8f3e | ||
|
|
f0b970c102 | ||
|
|
f05529c7d2 | ||
|
|
53818f3556 | ||
|
|
5c8471fe3f | ||
|
|
62eb032765 | ||
|
|
901b54805a | ||
|
|
0f2be88706 | ||
|
|
e66ca7c580 | ||
|
|
674dc03f46 | ||
|
|
cfeb20a18e | ||
|
|
4efdd6d834 | ||
|
|
e0a171051d | ||
|
|
18df989420 | ||
|
|
5a278d4424 | ||
|
|
fee3f500c5 | ||
|
|
80edfe7804 | ||
|
|
5b5a1e2fd8 | ||
|
|
09417bd6c1 | ||
|
|
6773fe0932 | ||
|
|
df9f791395 | ||
|
|
5e9cb77415 | ||
|
|
5ac3a903f6 | ||
|
|
8aefdbd948 | ||
|
|
f264725c45 | ||
|
|
5b07245cd9 | ||
|
|
c0542d0e94 | ||
|
|
8fdd173388 | ||
|
|
dc61f362fd | ||
|
|
4a008fbc3e | ||
|
|
7936c43b0b | ||
|
|
4881e4ef09 | ||
|
|
70a5ee9485 | ||
|
|
24aa7eac24 | ||
|
|
2ca90f2518 | ||
|
|
ff5e72e979 | ||
|
|
f37ad11ab8 | ||
|
|
1af0517f36 | ||
|
|
7305ad41ac | ||
|
|
ee48c7803c | ||
|
|
7a7093369f | ||
|
|
d6c0362404 | ||
|
|
0b2b0d1beb | ||
|
|
842b1c1fe5 | ||
|
|
714e8e862f | ||
|
|
9cb6084d31 | ||
|
|
f1d9757077 | ||
|
|
9dee0862cc | ||
|
|
c2bc8252f1 | ||
|
|
f808c8a471 | ||
|
|
28e0affbb4 | ||
|
|
39d339a3d8 | ||
|
|
00e1736d13 | ||
|
|
cfc441b9b1 | ||
|
|
3d7cf9fc93 | ||
|
|
f915b73f8d | ||
|
|
4121baac33 | ||
|
|
3e9f9289e5 | ||
|
|
9508b8d9b3 | ||
|
|
e261b4c0c5 | ||
|
|
23a08f30c4 | ||
|
|
08ae14222b | ||
|
|
e9a2744131 | ||
|
|
fff3f6d1cb | ||
|
|
55b9531d93 | ||
|
|
3434c437ce | ||
|
|
438f18f1b8 | ||
|
|
ac85c491fd | ||
|
|
a47a14fe95 | ||
|
|
be2260dc51 | ||
|
|
d1f0f4490c | ||
|
|
1c60a61f79 | ||
|
|
b5698acebf | ||
|
|
57b9f60ba3 | ||
|
|
b3ee622396 | ||
|
|
769f54e8dd | ||
|
|
2412e3be08 | ||
|
|
aed1474db8 | ||
|
|
b09a736a85 | ||
|
|
4030487472 |
712 changed files with 82646 additions and 31830 deletions
3622
.editorconfig
3622
.editorconfig
File diff suppressed because it is too large
Load diff
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
|
|
@ -10,26 +10,28 @@ jobs:
|
|||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 6.0.100
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
|
||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\"
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet build --no-restore --configuration Release --nologo
|
||||
- name: Archive
|
||||
run: Compress-Archive -Path Penumbra/bin/Release/net5.0-windows/* -DestinationPath Penumbra.zip
|
||||
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Penumbra/bin/Release/net5.0-windows/*
|
||||
./Penumbra/bin/Release/*
|
||||
|
|
|
|||
61
.github/workflows/release.yml
vendored
61
.github/workflows/release.yml
vendored
|
|
@ -3,43 +3,46 @@ name: Create Release
|
|||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- t*
|
||||
- testing_*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 6.0.100
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
|
||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||
- name: Build
|
||||
run: |
|
||||
$ver = '${{ github.ref }}' -replace 'refs/tags/',''
|
||||
$ver = '${{ github.ref_name }}'
|
||||
invoke-expression 'dotnet build --no-restore --configuration Release --nologo -p:Version=$ver -p:FileVersion=$ver -p:AssemblyVersion=$ver'
|
||||
- name: write version into json
|
||||
- name: write version into jsons
|
||||
run: |
|
||||
$ver = '${{ github.ref }}' -replace 'refs/tags/',''
|
||||
$path = './Penumbra/bin/Release/net5.0-windows/Penumbra.json'
|
||||
$content = get-content -path $path
|
||||
$content = $content -replace '1.0.0.0',$ver
|
||||
$ver = '${{ github.ref_name }}'
|
||||
$path = './Penumbra/bin/Release/Penumbra.json'
|
||||
$json = Get-Content -Raw $path | ConvertFrom-Json
|
||||
$json.AssemblyVersion = $ver
|
||||
$content = $json | ConvertTo-Json
|
||||
set-content -Path $path -Value $content
|
||||
- name: Archive
|
||||
run: Compress-Archive -Path Penumbra/bin/Release/net5.0-windows/* -DestinationPath Penumbra.zip
|
||||
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Penumbra/bin/Release/net5.0-windows/*
|
||||
./Penumbra/bin/Release/*
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
|
|
@ -63,22 +66,24 @@ jobs:
|
|||
|
||||
- name: Write out repo.json
|
||||
run: |
|
||||
$ver = '${{ github.ref }}' -replace 'refs/tags/',''
|
||||
$path = './base_repo.json'
|
||||
$new_path = './repo.json'
|
||||
$content = get-content -path $path
|
||||
$content = $content -replace '1.0.0.0',$ver
|
||||
set-content -Path $new_path -Value $content
|
||||
$ver = '${{ github.ref_name }}'
|
||||
$path = './repo.json'
|
||||
$json = Get-Content -Raw $path | ConvertFrom-Json
|
||||
$json[0].AssemblyVersion = $ver
|
||||
$json[0].TestingAssemblyVersion = $ver
|
||||
$json[0].DownloadLinkInstall = $json.DownloadLinkInstall -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip"
|
||||
$json[0].DownloadLinkTesting = $json.DownloadLinkTesting -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip"
|
||||
$json[0].DownloadLinkUpdate = $json.DownloadLinkUpdate -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip"
|
||||
$content = $json | ConvertTo-Json -AsArray
|
||||
set-content -Path $path -Value $content
|
||||
|
||||
- name: Commit repo.json
|
||||
run: |
|
||||
git config --global user.name "Actions User"
|
||||
git config --global user.email "actions@github.com"
|
||||
|
||||
git fetch origin master && git fetch origin test && git checkout master
|
||||
git fetch origin master
|
||||
git branch -f master ${{ github.sha }}
|
||||
git checkout master
|
||||
git add repo.json
|
||||
git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true
|
||||
|
||||
git push origin master || true
|
||||
git branch -f test origin/master && git checkout test
|
||||
git push origin test -f || true
|
||||
git commit -m "[CI] Updating repo.json for ${{ github.ref_name }}" || true
|
||||
git push origin master
|
||||
|
|
|
|||
57
.github/workflows/test_release.yml
vendored
57
.github/workflows/test_release.yml
vendored
|
|
@ -3,43 +3,46 @@ name: Create Test Release
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- t*
|
||||
- testing_*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 6.0.100
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
|
||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||
- name: Build
|
||||
run: |
|
||||
$ver = '${{ github.ref }}' -replace 'refs/tags/t',''
|
||||
$ver = '${{ github.ref_name }}' -replace 'testing_'
|
||||
invoke-expression 'dotnet build --no-restore --configuration Debug --nologo -p:Version=$ver -p:FileVersion=$ver -p:AssemblyVersion=$ver'
|
||||
- name: write version into json
|
||||
run: |
|
||||
$ver = '${{ github.ref }}' -replace 'refs/tags/t',''
|
||||
$path = './Penumbra/bin/Debug/net5.0-windows/Penumbra.json'
|
||||
$content = get-content -path $path
|
||||
$content = $content -replace '1.0.0.0',$ver
|
||||
$ver = '${{ github.ref_name }}' -replace 'testing_'
|
||||
$path = './Penumbra/bin/Debug/Penumbra.json'
|
||||
$json = Get-Content -Raw $path | ConvertFrom-Json
|
||||
$json.AssemblyVersion = $ver
|
||||
$content = $json | ConvertTo-Json
|
||||
set-content -Path $path -Value $content
|
||||
- name: Archive
|
||||
run: Compress-Archive -Path Penumbra/bin/Debug/net5.0-windows/* -DestinationPath Penumbra.zip
|
||||
run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Penumbra/bin/Debug/net5.0-windows/*
|
||||
./Penumbra/bin/Debug/*
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
|
|
@ -63,22 +66,22 @@ jobs:
|
|||
|
||||
- name: Write out repo.json
|
||||
run: |
|
||||
$ver = '${{ github.ref }}' -replace 'refs/tags/t',''
|
||||
$ver2 = '${{ github.ref }}' -replace 'refs/tags/',''
|
||||
$path = './base_repo.json'
|
||||
$new_path = './repo.json'
|
||||
$content = get-content -path $path
|
||||
$content = $content -replace '/1.0.0.0/',"/$ver2/"
|
||||
$content = $content -replace '1.0.0.0',$ver
|
||||
set-content -Path $new_path -Value $content
|
||||
$verT = '${{ github.ref_name }}'
|
||||
$ver = $verT -replace 'testing_'
|
||||
$path = './repo.json'
|
||||
$json = Get-Content -Raw $path | ConvertFrom-Json
|
||||
$json[0].TestingAssemblyVersion = $ver
|
||||
$json[0].DownloadLinkTesting = $json.DownloadLinkTesting -replace '[^/]+/Penumbra.zip',"$verT/Penumbra.zip"
|
||||
$content = $json | ConvertTo-Json -AsArray
|
||||
set-content -Path $path -Value $content
|
||||
|
||||
- name: Commit repo.json
|
||||
run: |
|
||||
git config --global user.name "Actions User"
|
||||
git config --global user.email "actions@github.com"
|
||||
|
||||
git fetch origin master && git fetch origin test && git branch -f test origin/master && git checkout test
|
||||
git fetch origin master
|
||||
git branch -f master ${{ github.sha }}
|
||||
git checkout master
|
||||
git add repo.json
|
||||
git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true
|
||||
|
||||
git push origin test -f || true
|
||||
git commit -m "[CI] Updating repo.json for ${{ github.ref_name }}" || true
|
||||
git push origin master
|
||||
|
|
|
|||
18
.gitmodules
vendored
18
.gitmodules
vendored
|
|
@ -1,4 +1,16 @@
|
|||
[submodule "OtterGui"]
|
||||
path = OtterGui
|
||||
url = git@github.com:Ottermandias/OtterGui.git
|
||||
[submodule "OtterGui"]
|
||||
path = OtterGui
|
||||
url = https://github.com/Ottermandias/OtterGui.git
|
||||
branch = main
|
||||
[submodule "Penumbra.Api"]
|
||||
path = Penumbra.Api
|
||||
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||
branch = main
|
||||
[submodule "Penumbra.String"]
|
||||
path = Penumbra.String
|
||||
url = https://github.com/Ottermandias/Penumbra.String.git
|
||||
branch = main
|
||||
[submodule "Penumbra.GameData"]
|
||||
path = Penumbra.GameData
|
||||
url = https://github.com/Ottermandias/Penumbra.GameData.git
|
||||
branch = main
|
||||
|
|
|
|||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit fcc5031fcf14b54f090d5bb789c580567b3f0023
|
||||
Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf
|
||||
1
Penumbra.Api
Submodule
1
Penumbra.Api
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 52a3216a525592205198303df2844435e382cf87
|
||||
123
Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
Normal file
123
Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Penumbra.CrashHandler.Buffers;
|
||||
|
||||
/// <summary> The types of currently hooked and relevant animation loading functions. </summary>
|
||||
public enum AnimationInvocationType : int
|
||||
{
|
||||
PapLoad,
|
||||
ActionLoad,
|
||||
ScheduleClipUpdate,
|
||||
LoadTimelineResources,
|
||||
LoadCharacterVfx,
|
||||
LoadCharacterSound,
|
||||
ApricotSoundPlay,
|
||||
LoadAreaVfx,
|
||||
CharacterBaseLoadAnimation,
|
||||
}
|
||||
|
||||
/// <summary> The full crash entry for an invoked vfx function. </summary>
|
||||
public record struct VfxFuncInvokedEntry(
|
||||
double Age,
|
||||
DateTimeOffset Timestamp,
|
||||
int ThreadId,
|
||||
string InvocationType,
|
||||
string CharacterName,
|
||||
string CharacterAddress,
|
||||
Guid CollectionId) : ICrashDataEntry;
|
||||
|
||||
/// <summary> Only expose the write interface for the buffer. </summary>
|
||||
public interface IAnimationInvocationBufferWriter
|
||||
{
|
||||
/// <summary> Write a line into the buffer with the given data. </summary>
|
||||
/// <param name="characterAddress"> The address of the related character, if known. </param>
|
||||
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
|
||||
/// <param name="collectionId"> The GUID of the associated collection. </param>
|
||||
/// <param name="type"> The type of VFX func called. </param>
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, AnimationInvocationType type);
|
||||
}
|
||||
|
||||
internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader
|
||||
{
|
||||
private const int _version = 1;
|
||||
private const int _lineCount = 64;
|
||||
private const int _lineCapacity = 128;
|
||||
private const string _name = "Penumbra.AnimationInvocation";
|
||||
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, AnimationInvocationType type)
|
||||
{
|
||||
var accessor = GetCurrentLineLocking();
|
||||
lock (accessor)
|
||||
{
|
||||
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
accessor.Write(8, Environment.CurrentManagedThreadId);
|
||||
accessor.Write(12, (int)type);
|
||||
accessor.Write(16, characterAddress);
|
||||
var span = GetSpan(accessor, 24, 16);
|
||||
collectionId.TryWriteBytes(span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
span = GetSpan(accessor, 40);
|
||||
WriteSpan(characterName, span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
|
||||
public uint TotalCount
|
||||
=> TotalWrittenLines;
|
||||
|
||||
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
|
||||
{
|
||||
var lineCount = (int)CurrentLineCount;
|
||||
for (var i = lineCount - 1; i >= 0; --i)
|
||||
{
|
||||
var line = GetLine(i);
|
||||
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
|
||||
var thread = BitConverter.ToInt32(line[8..]);
|
||||
var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]);
|
||||
var address = BitConverter.ToUInt64(line[16..]);
|
||||
var collectionId = new Guid(line[24..40]);
|
||||
var characterName = ReadString(line[40..]);
|
||||
yield return new JsonObject()
|
||||
{
|
||||
[nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
|
||||
[nameof(VfxFuncInvokedEntry.Timestamp)] = timestamp,
|
||||
[nameof(VfxFuncInvokedEntry.ThreadId)] = thread,
|
||||
[nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type),
|
||||
[nameof(VfxFuncInvokedEntry.CharacterName)] = characterName,
|
||||
[nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"),
|
||||
[nameof(VfxFuncInvokedEntry.CollectionId)] = collectionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static IBufferReader CreateReader(int pid)
|
||||
=> new AnimationInvocationBuffer(false, pid);
|
||||
|
||||
public static IAnimationInvocationBufferWriter CreateWriter(int pid)
|
||||
=> new AnimationInvocationBuffer(pid);
|
||||
|
||||
private AnimationInvocationBuffer(bool writer, int pid)
|
||||
: base($"{_name}_{pid}_{_version}", _version)
|
||||
{ }
|
||||
|
||||
private AnimationInvocationBuffer(int pid)
|
||||
: base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity)
|
||||
{ }
|
||||
|
||||
private static string ToName(AnimationInvocationType type)
|
||||
=> type switch
|
||||
{
|
||||
AnimationInvocationType.PapLoad => "PAP Load",
|
||||
AnimationInvocationType.ActionLoad => "Action Load",
|
||||
AnimationInvocationType.ScheduleClipUpdate => "Schedule Clip Update",
|
||||
AnimationInvocationType.LoadTimelineResources => "Load Timeline Resources",
|
||||
AnimationInvocationType.LoadCharacterVfx => "Load Character VFX",
|
||||
AnimationInvocationType.LoadCharacterSound => "Load Character Sound",
|
||||
AnimationInvocationType.ApricotSoundPlay => "Apricot Sound Play",
|
||||
AnimationInvocationType.LoadAreaVfx => "Load Area VFX",
|
||||
AnimationInvocationType.CharacterBaseLoadAnimation => "Load Animation (CharacterBase)",
|
||||
_ => $"Unknown ({(int)type})",
|
||||
};
|
||||
}
|
||||
89
Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
Normal file
89
Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Penumbra.CrashHandler.Buffers;
|
||||
|
||||
/// <summary> Only expose the write interface for the buffer. </summary>
|
||||
public interface ICharacterBaseBufferWriter
|
||||
{
|
||||
/// <summary> Write a line into the buffer with the given data. </summary>
|
||||
/// <param name="characterAddress"> The address of the related character, if known. </param>
|
||||
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
|
||||
/// <param name="collectionId"> The GUID of the associated collection. </param>
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId);
|
||||
}
|
||||
|
||||
/// <summary> The full crash entry for a loaded character base. </summary>
|
||||
public record struct CharacterLoadedEntry(
|
||||
double Age,
|
||||
DateTimeOffset Timestamp,
|
||||
int ThreadId,
|
||||
string CharacterName,
|
||||
string CharacterAddress,
|
||||
Guid CollectionId) : ICrashDataEntry;
|
||||
|
||||
internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader
|
||||
{
|
||||
private const int _version = 1;
|
||||
private const int _lineCount = 10;
|
||||
private const int _lineCapacity = 128;
|
||||
private const string _name = "Penumbra.CharacterBase";
|
||||
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId)
|
||||
{
|
||||
var accessor = GetCurrentLineLocking();
|
||||
lock (accessor)
|
||||
{
|
||||
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
accessor.Write(8, Environment.CurrentManagedThreadId);
|
||||
accessor.Write(12, characterAddress);
|
||||
var span = GetSpan(accessor, 20, 16);
|
||||
collectionId.TryWriteBytes(span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
span = GetSpan(accessor, 36);
|
||||
WriteSpan(characterName, span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
|
||||
{
|
||||
var lineCount = (int)CurrentLineCount;
|
||||
for (var i = lineCount - 1; i >= 0; --i)
|
||||
{
|
||||
var line = GetLine(i);
|
||||
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
|
||||
var thread = BitConverter.ToInt32(line[8..]);
|
||||
var address = BitConverter.ToUInt64(line[12..]);
|
||||
var collectionId = new Guid(line[20..36]);
|
||||
var characterName = ReadString(line[36..]);
|
||||
yield return new JsonObject
|
||||
{
|
||||
[nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
|
||||
[nameof(CharacterLoadedEntry.Timestamp)] = timestamp,
|
||||
[nameof(CharacterLoadedEntry.ThreadId)] = thread,
|
||||
[nameof(CharacterLoadedEntry.CharacterName)] = characterName,
|
||||
[nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"),
|
||||
[nameof(CharacterLoadedEntry.CollectionId)] = collectionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public uint TotalCount
|
||||
=> TotalWrittenLines;
|
||||
|
||||
public static IBufferReader CreateReader(int pid)
|
||||
=> new CharacterBaseBuffer(false, pid);
|
||||
|
||||
public static ICharacterBaseBufferWriter CreateWriter(int pid)
|
||||
=> new CharacterBaseBuffer(pid);
|
||||
|
||||
private CharacterBaseBuffer(bool writer, int pid)
|
||||
: base($"{_name}_{pid}_{_version}", _version)
|
||||
{ }
|
||||
|
||||
private CharacterBaseBuffer(int pid)
|
||||
: base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity)
|
||||
{ }
|
||||
}
|
||||
220
Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
Normal file
220
Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace Penumbra.CrashHandler.Buffers;
|
||||
|
||||
public class MemoryMappedBuffer : IDisposable
|
||||
{
|
||||
private const int MinHeaderLength = 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4;
|
||||
|
||||
private readonly MemoryMappedFile _file;
|
||||
private readonly MemoryMappedViewAccessor _header;
|
||||
private readonly MemoryMappedViewAccessor[] _lines = [];
|
||||
|
||||
public readonly int Version;
|
||||
public readonly uint LineCount;
|
||||
public readonly uint LineCapacity;
|
||||
private readonly uint _lineMask;
|
||||
private bool _disposed;
|
||||
|
||||
protected uint CurrentLineCount
|
||||
{
|
||||
get => _header.ReadUInt32(16);
|
||||
set => _header.Write(16, value);
|
||||
}
|
||||
|
||||
protected uint CurrentLinePosition
|
||||
{
|
||||
get => _header.ReadUInt32(20);
|
||||
set => _header.Write(20, value);
|
||||
}
|
||||
|
||||
public uint TotalWrittenLines
|
||||
{
|
||||
get => _header.ReadUInt32(24);
|
||||
protected set => _header.Write(24, value);
|
||||
}
|
||||
|
||||
public MemoryMappedBuffer(string mapName, int version, uint lineCount, uint lineCapacity)
|
||||
{
|
||||
Version = version;
|
||||
LineCount = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCount, 2, int.MaxValue >> 3));
|
||||
LineCapacity = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCapacity, 2, int.MaxValue >> 3));
|
||||
_lineMask = LineCount - 1;
|
||||
var fileName = Encoding.UTF8.GetBytes(mapName);
|
||||
var headerLength = (uint)(4 + 4 + 4 + 4 + 4 + 4 + 4 + fileName.Length + 1);
|
||||
headerLength = (headerLength & 0b111) > 0 ? (headerLength & ~0b111u) + 0b1000 : headerLength;
|
||||
var capacity = LineCount * LineCapacity + headerLength;
|
||||
_file = MemoryMappedFile.CreateNew(mapName, capacity, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None,
|
||||
HandleInheritability.Inheritable);
|
||||
_header = _file.CreateViewAccessor(0, headerLength);
|
||||
_header.Write(0, headerLength);
|
||||
_header.Write(4, Version);
|
||||
_header.Write(8, LineCount);
|
||||
_header.Write(12, LineCapacity);
|
||||
_header.WriteArray(28, fileName, 0, fileName.Length);
|
||||
_header.Write(fileName.Length + 28, (byte)0);
|
||||
_lines = Enumerable.Range(0, (int)LineCount).Select(i
|
||||
=> _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public MemoryMappedBuffer(string mapName, int? expectedVersion = null, uint? expectedMinLineCount = null,
|
||||
uint? expectedMinLineCapacity = null)
|
||||
{
|
||||
_lines = [];
|
||||
_file = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.ReadWrite, HandleInheritability.Inheritable);
|
||||
using var headerLine = _file.CreateViewAccessor(0, 4, MemoryMappedFileAccess.Read);
|
||||
var headerLength = headerLine.ReadUInt32(0);
|
||||
if (headerLength < MinHeaderLength)
|
||||
Throw($"Map {mapName} did not contain a valid header.");
|
||||
|
||||
_header = _file.CreateViewAccessor(0, headerLength, MemoryMappedFileAccess.ReadWrite);
|
||||
Version = _header.ReadInt32(4);
|
||||
LineCount = _header.ReadUInt32(8);
|
||||
LineCapacity = _header.ReadUInt32(12);
|
||||
_lineMask = LineCount - 1;
|
||||
if (expectedVersion.HasValue && expectedVersion.Value != Version)
|
||||
Throw($"Map {mapName} has version {Version} instead of {expectedVersion.Value}.");
|
||||
|
||||
if (LineCount < expectedMinLineCount)
|
||||
Throw($"Map {mapName} has line count {LineCount} but line count >= {expectedMinLineCount.Value} is required.");
|
||||
|
||||
if (LineCapacity < expectedMinLineCapacity)
|
||||
Throw($"Map {mapName} has line capacity {LineCapacity} but line capacity >= {expectedMinLineCapacity.Value} is required.");
|
||||
|
||||
var name = ReadString(GetSpan(_header, 28));
|
||||
if (name != mapName)
|
||||
Throw($"Map {mapName} does not contain its map name at the expected location.");
|
||||
|
||||
_lines = Enumerable.Range(0, (int)LineCount).Select(i
|
||||
=> _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite))
|
||||
.ToArray();
|
||||
|
||||
[DoesNotReturn]
|
||||
void Throw(string text)
|
||||
{
|
||||
_file.Dispose();
|
||||
_header?.Dispose();
|
||||
_disposed = true;
|
||||
throw new Exception(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
protected static string ReadString(Span<byte> span)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
throw new Exception("String from empty span requested.");
|
||||
|
||||
var termination = span.IndexOf((byte)0);
|
||||
if (termination < 0)
|
||||
throw new Exception("String in span is not terminated.");
|
||||
|
||||
return Encoding.UTF8.GetString(span[..termination]);
|
||||
}
|
||||
|
||||
protected static int WriteString(string text, Span<byte> span)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
var source = (Span<byte>)bytes;
|
||||
var length = source.Length + 1;
|
||||
if (length > span.Length)
|
||||
source = source[..(span.Length - 1)];
|
||||
source.CopyTo(span);
|
||||
span[bytes.Length] = 0;
|
||||
return source.Length + 1;
|
||||
}
|
||||
|
||||
protected static int WriteSpan(ReadOnlySpan<byte> input, Span<byte> span)
|
||||
{
|
||||
var length = input.Length + 1;
|
||||
if (length > span.Length)
|
||||
input = input[..(span.Length - 1)];
|
||||
|
||||
input.CopyTo(span);
|
||||
span[input.Length] = 0;
|
||||
return input.Length + 1;
|
||||
}
|
||||
|
||||
protected Span<byte> GetLine(int i)
|
||||
{
|
||||
if (i < 0 || i > LineCount)
|
||||
return null;
|
||||
|
||||
lock (_header)
|
||||
{
|
||||
var lineIdx = (CurrentLinePosition + i) & _lineMask;
|
||||
if (lineIdx > CurrentLineCount)
|
||||
return null;
|
||||
|
||||
return GetSpan(_lines[lineIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected MemoryMappedViewAccessor GetCurrentLineLocking()
|
||||
{
|
||||
MemoryMappedViewAccessor view;
|
||||
lock (_header)
|
||||
{
|
||||
var currentLineCount = CurrentLineCount;
|
||||
if (currentLineCount == LineCount)
|
||||
{
|
||||
var currentLinePos = CurrentLinePosition;
|
||||
view = _lines[currentLinePos]!;
|
||||
CurrentLinePosition = (currentLinePos + 1) & _lineMask;
|
||||
}
|
||||
else
|
||||
{
|
||||
view = _lines[currentLineCount];
|
||||
++CurrentLineCount;
|
||||
}
|
||||
|
||||
++TotalWrittenLines;
|
||||
_header.Flush();
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
protected static Span<byte> GetSpan(MemoryMappedViewAccessor accessor, int offset = 0)
|
||||
=> GetSpan(accessor, offset, (int)accessor.Capacity - offset);
|
||||
|
||||
protected static unsafe Span<byte> GetSpan(MemoryMappedViewAccessor accessor, int offset, int size)
|
||||
{
|
||||
byte* ptr = null;
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
|
||||
size = Math.Min(size, (int)accessor.Capacity - offset);
|
||||
if (size < 0)
|
||||
return [];
|
||||
|
||||
var span = new Span<byte>(ptr + offset + accessor.PointerOffset, size);
|
||||
return span;
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_header?.Dispose();
|
||||
foreach (var line in _lines)
|
||||
line?.Dispose();
|
||||
_file?.Dispose();
|
||||
}
|
||||
|
||||
~MemoryMappedBuffer()
|
||||
=> Dispose(false);
|
||||
}
|
||||
105
Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
Normal file
105
Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Penumbra.CrashHandler.Buffers;
|
||||
|
||||
/// <summary> Only expose the write interface for the buffer. </summary>
|
||||
public interface IModdedFileBufferWriter
|
||||
{
|
||||
/// <summary> Write a line into the buffer with the given data. </summary>
|
||||
/// <param name="characterAddress"> The address of the related character, if known. </param>
|
||||
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
|
||||
/// <param name="collectionId"> The GUID of the associated collection. </param>
|
||||
/// <param name="requestedFileName"> The file name as requested by the game. </param>
|
||||
/// <param name="actualFileName"> The actual modded file name loaded. </param>
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
|
||||
ReadOnlySpan<byte> actualFileName);
|
||||
}
|
||||
|
||||
/// <summary> The full crash entry for a loaded modded file. </summary>
|
||||
public record struct ModdedFileLoadedEntry(
|
||||
double Age,
|
||||
DateTimeOffset Timestamp,
|
||||
int ThreadId,
|
||||
string CharacterName,
|
||||
string CharacterAddress,
|
||||
Guid CollectionId,
|
||||
string RequestedFileName,
|
||||
string ActualFileName) : ICrashDataEntry;
|
||||
|
||||
internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader
|
||||
{
|
||||
private const int _version = 1;
|
||||
private const int _lineCount = 128;
|
||||
private const int _lineCapacity = 1024;
|
||||
private const string _name = "Penumbra.ModdedFile";
|
||||
|
||||
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
|
||||
ReadOnlySpan<byte> actualFileName)
|
||||
{
|
||||
var accessor = GetCurrentLineLocking();
|
||||
lock (accessor)
|
||||
{
|
||||
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
accessor.Write(8, Environment.CurrentManagedThreadId);
|
||||
accessor.Write(12, characterAddress);
|
||||
var span = GetSpan(accessor, 20, 16);
|
||||
collectionId.TryWriteBytes(span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
span = GetSpan(accessor, 36, 80);
|
||||
WriteSpan(characterName, span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
span = GetSpan(accessor, 116, 260);
|
||||
WriteSpan(requestedFileName, span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
span = GetSpan(accessor, 376);
|
||||
WriteSpan(actualFileName, span);
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
|
||||
public uint TotalCount
|
||||
=> TotalWrittenLines;
|
||||
|
||||
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
|
||||
{
|
||||
var lineCount = (int)CurrentLineCount;
|
||||
for (var i = lineCount - 1; i >= 0; --i)
|
||||
{
|
||||
var line = GetLine(i);
|
||||
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
|
||||
var thread = BitConverter.ToInt32(line[8..]);
|
||||
var address = BitConverter.ToUInt64(line[12..]);
|
||||
var collectionId = new Guid(line[20..36]);
|
||||
var characterName = ReadString(line[36..]);
|
||||
var requestedFileName = ReadString(line[116..]);
|
||||
var actualFileName = ReadString(line[376..]);
|
||||
yield return new JsonObject()
|
||||
{
|
||||
[nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
|
||||
[nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp,
|
||||
[nameof(ModdedFileLoadedEntry.ThreadId)] = thread,
|
||||
[nameof(ModdedFileLoadedEntry.CharacterName)] = characterName,
|
||||
[nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"),
|
||||
[nameof(ModdedFileLoadedEntry.CollectionId)] = collectionId,
|
||||
[nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName,
|
||||
[nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static IBufferReader CreateReader(int pid)
|
||||
=> new ModdedFileBuffer(false, pid);
|
||||
|
||||
public static IModdedFileBufferWriter CreateWriter(int pid)
|
||||
=> new ModdedFileBuffer(pid);
|
||||
|
||||
private ModdedFileBuffer(bool writer, int pid)
|
||||
: base($"{_name}_{pid}_{_version}", _version)
|
||||
{ }
|
||||
|
||||
private ModdedFileBuffer(int pid)
|
||||
: base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity)
|
||||
{ }
|
||||
}
|
||||
70
Penumbra.CrashHandler/CrashData.cs
Normal file
70
Penumbra.CrashHandler/CrashData.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
/// <summary> A base entry for crash data. </summary>
|
||||
public interface ICrashDataEntry
|
||||
{
|
||||
/// <summary> The timestamp of the event. </summary>
|
||||
DateTimeOffset Timestamp { get; }
|
||||
|
||||
/// <summary> The thread invoking the event. </summary>
|
||||
int ThreadId { get; }
|
||||
|
||||
/// <summary> The age of the event compared to the crash. (Redundantly with the timestamp) </summary>
|
||||
double Age { get; }
|
||||
}
|
||||
|
||||
/// <summary> A full set of crash data. </summary>
|
||||
public class CrashData
|
||||
{
|
||||
/// <summary> The mode this data was obtained - manually or from a crash. </summary>
|
||||
public string Mode { get; set; } = "Unknown";
|
||||
|
||||
/// <summary> The time this crash data was generated. </summary>
|
||||
public DateTimeOffset CrashTime { get; set; } = DateTimeOffset.UnixEpoch;
|
||||
|
||||
/// <summary> Penumbra's Version when this crash data was created. </summary>
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> The Game's Version when this crash data was created. </summary>
|
||||
public string GameVersion { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> The FFXIV process ID when this data was generated. </summary>
|
||||
public int ProcessId { get; set; } = 0;
|
||||
|
||||
/// <summary> The FFXIV Exit Code (if any) when this data was generated. </summary>
|
||||
public int ExitCode { get; set; } = 0;
|
||||
|
||||
/// <summary> The total amount of characters loaded during this session. </summary>
|
||||
public int TotalCharactersLoaded { get; set; } = 0;
|
||||
|
||||
/// <summary> The total amount of modded files loaded during this session. </summary>
|
||||
public int TotalModdedFilesLoaded { get; set; } = 0;
|
||||
|
||||
/// <summary> The total amount of vfx functions invoked during this session. </summary>
|
||||
public int TotalVFXFuncsInvoked { get; set; } = 0;
|
||||
|
||||
/// <summary> The last character loaded before this crash data was generated. </summary>
|
||||
public CharacterLoadedEntry? LastCharacterLoaded
|
||||
=> LastCharactersLoaded.Count == 0 ? default : LastCharactersLoaded[0];
|
||||
|
||||
/// <summary> The last modded file loaded before this crash data was generated. </summary>
|
||||
public ModdedFileLoadedEntry? LastModdedFileLoaded
|
||||
=> LastModdedFilesLoaded.Count == 0 ? default : LastModdedFilesLoaded[0];
|
||||
|
||||
/// <summary> The last vfx function invoked before this crash data was generated. </summary>
|
||||
public VfxFuncInvokedEntry? LastVfxFuncInvoked
|
||||
=> LastVFXFuncsInvoked.Count == 0 ? default : LastVFXFuncsInvoked[0];
|
||||
|
||||
/// <summary> A collection of the last few characters loaded before this crash data was generated. </summary>
|
||||
public List<CharacterLoadedEntry> LastCharactersLoaded { get; set; } = [];
|
||||
|
||||
/// <summary> A collection of the last few modded files loaded before this crash data was generated. </summary>
|
||||
public List<ModdedFileLoadedEntry> LastModdedFilesLoaded { get; set; } = [];
|
||||
|
||||
/// <summary> A collection of the last few vfx functions invoked before this crash data was generated. </summary>
|
||||
public List<VfxFuncInvokedEntry> LastVFXFuncsInvoked { get; set; } = [];
|
||||
}
|
||||
58
Penumbra.CrashHandler/GameEventLogReader.cs
Normal file
58
Penumbra.CrashHandler/GameEventLogReader.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public interface IBufferReader
|
||||
{
|
||||
public uint TotalCount { get; }
|
||||
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime);
|
||||
}
|
||||
|
||||
public sealed class GameEventLogReader(int pid) : IDisposable
|
||||
{
|
||||
public readonly (IBufferReader Reader, string TypeSingular, string TypePlural)[] Readers =
|
||||
[
|
||||
(CharacterBaseBuffer.CreateReader(pid), "CharacterLoaded", "CharactersLoaded"),
|
||||
(ModdedFileBuffer.CreateReader(pid), "ModdedFileLoaded", "ModdedFilesLoaded"),
|
||||
(AnimationInvocationBuffer.CreateReader(pid), "VFXFuncInvoked", "VFXFuncsInvoked"),
|
||||
];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var (reader, _, _) in Readers)
|
||||
(reader as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public JsonObject Dump(string mode, int processId, int exitCode, string version, string gameVersion)
|
||||
{
|
||||
var crashTime = DateTimeOffset.UtcNow;
|
||||
var obj = new JsonObject
|
||||
{
|
||||
[nameof(CrashData.Mode)] = mode,
|
||||
[nameof(CrashData.CrashTime)] = DateTimeOffset.UtcNow,
|
||||
[nameof(CrashData.ProcessId)] = processId,
|
||||
[nameof(CrashData.ExitCode)] = exitCode,
|
||||
[nameof(CrashData.Version)] = version,
|
||||
[nameof(CrashData.GameVersion)] = gameVersion,
|
||||
};
|
||||
|
||||
foreach (var (reader, singular, _) in Readers)
|
||||
obj["Last" + singular] = reader.GetLines(crashTime).FirstOrDefault();
|
||||
|
||||
foreach (var (reader, _, plural) in Readers)
|
||||
{
|
||||
obj["Total" + plural] = reader.TotalCount;
|
||||
var array = new JsonArray();
|
||||
foreach (var file in reader.GetLines(crashTime))
|
||||
array.Add(file);
|
||||
obj["Last" + plural] = array;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
18
Penumbra.CrashHandler/GameEventLogWriter.cs
Normal file
18
Penumbra.CrashHandler/GameEventLogWriter.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public sealed class GameEventLogWriter(int pid) : IDisposable
|
||||
{
|
||||
public readonly ICharacterBaseBufferWriter CharacterBase = CharacterBaseBuffer.CreateWriter(pid);
|
||||
public readonly IModdedFileBufferWriter FileLoaded = ModdedFileBuffer.CreateWriter(pid);
|
||||
public readonly IAnimationInvocationBufferWriter AnimationFuncInvoked = AnimationInvocationBuffer.CreateWriter(pid);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(CharacterBase as IDisposable)?.Dispose();
|
||||
(FileLoaded as IDisposable)?.Dispose();
|
||||
(AnimationFuncInvoked as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
18
Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
Normal file
18
Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Use_DalamudPackager>false</Use_DalamudPackager>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
41
Penumbra.CrashHandler/Program.cs
Normal file
41
Penumbra.CrashHandler/Program.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public class CrashHandler
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length < 4 || !int.TryParse(args[1], out var pid))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new GameEventLogReader(pid);
|
||||
var parent = Process.GetProcessById(pid);
|
||||
using var handle = parent.SafeHandle;
|
||||
parent.WaitForExit();
|
||||
int exitCode;
|
||||
try
|
||||
{
|
||||
exitCode = parent.ExitCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
var obj = reader.Dump("Crash", pid, exitCode, args[2], args[3]);
|
||||
using var fs = File.Open(args[0], FileMode.Create);
|
||||
using var w = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true });
|
||||
obj.WriteTo(w, new JsonSerializerOptions() { WriteIndented = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(args[0], $"{DateTime.UtcNow} {pid} {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Penumbra.CrashHandler/packages.lock.json
Normal file
13
Penumbra.CrashHandler/packages.lock.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Penumbra.GameData
Submodule
1
Penumbra.GameData
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
public static unsafe partial class ByteStringFunctions
|
||||
{
|
||||
private static readonly byte[] AsciiLowerCaseBytes = Enumerable.Range( 0, 256 )
|
||||
.Select( i => ( byte )char.ToLowerInvariant( ( char )i ) )
|
||||
.ToArray();
|
||||
|
||||
// Convert a byte to its ASCII-lowercase version.
|
||||
public static byte AsciiToLower( byte b )
|
||||
=> AsciiLowerCaseBytes[ b ];
|
||||
|
||||
// Check if a byte is ASCII-lowercase.
|
||||
public static bool AsciiIsLower( byte b )
|
||||
=> AsciiToLower( b ) == b;
|
||||
|
||||
// Check if a byte array of given length is ASCII-lowercase.
|
||||
public static bool IsAsciiLowerCase( byte* path, int length )
|
||||
{
|
||||
var end = path + length;
|
||||
for( ; path < end; ++path )
|
||||
{
|
||||
if( *path != AsciiLowerCaseBytes[*path] )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare two byte arrays of given lengths ASCII-case-insensitive.
|
||||
public static int AsciiCaselessCompare( byte* lhs, int lhsLength, byte* rhs, int rhsLength )
|
||||
{
|
||||
if( lhsLength == rhsLength )
|
||||
{
|
||||
return lhs == rhs ? 0 : Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength );
|
||||
}
|
||||
|
||||
if( lhsLength < rhsLength )
|
||||
{
|
||||
var cmp = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength );
|
||||
return cmp != 0 ? cmp : -1;
|
||||
}
|
||||
|
||||
var cmp2 = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength );
|
||||
return cmp2 != 0 ? cmp2 : 1;
|
||||
}
|
||||
|
||||
// Check two byte arrays of given lengths for ASCII-case-insensitive equality.
|
||||
public static bool AsciiCaselessEquals( byte* lhs, int lhsLength, byte* rhs, int rhsLength )
|
||||
{
|
||||
if( lhsLength != rhsLength )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( lhs == rhs || lhsLength == 0 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ) == 0;
|
||||
}
|
||||
|
||||
// Check if a byte array of given length consists purely of ASCII characters.
|
||||
public static bool IsAscii( byte* path, int length )
|
||||
{
|
||||
var length8 = length / 8;
|
||||
var end8 = ( ulong* )path + length8;
|
||||
for( var ptr8 = ( ulong* )path; ptr8 < end8; ++ptr8 )
|
||||
{
|
||||
if( ( *ptr8 & 0x8080808080808080ul ) != 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var end = path + length;
|
||||
for( path += length8 * 8; path < end; ++path )
|
||||
{
|
||||
if( *path > 127 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
public static unsafe partial class ByteStringFunctions
|
||||
{
|
||||
// Lexicographically compare two byte arrays of given length.
|
||||
public static int Compare( byte* lhs, int lhsLength, byte* rhs, int rhsLength )
|
||||
{
|
||||
if( lhsLength == rhsLength )
|
||||
{
|
||||
return lhs == rhs ? 0 : Functions.MemCmpUnchecked( lhs, rhs, rhsLength );
|
||||
}
|
||||
|
||||
if( lhsLength < rhsLength )
|
||||
{
|
||||
var cmp = Functions.MemCmpUnchecked( lhs, rhs, lhsLength );
|
||||
return cmp != 0 ? cmp : -1;
|
||||
}
|
||||
|
||||
var cmp2 = Functions.MemCmpUnchecked( lhs, rhs, rhsLength );
|
||||
return cmp2 != 0 ? cmp2 : 1;
|
||||
}
|
||||
|
||||
// Lexicographically compare one byte array of given length with a null-terminated byte array of unknown length.
|
||||
public static int Compare( byte* lhs, int lhsLength, byte* rhs )
|
||||
{
|
||||
var end = lhs + lhsLength;
|
||||
for( var tmp = lhs; tmp < end; ++tmp, ++rhs )
|
||||
{
|
||||
if( *rhs == 0 )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var diff = *tmp - *rhs;
|
||||
if( diff != 0 )
|
||||
{
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lexicographically compare two null-terminated byte arrays of unknown length not larger than maxLength.
|
||||
public static int Compare( byte* lhs, byte* rhs, int maxLength = int.MaxValue )
|
||||
{
|
||||
var end = lhs + maxLength;
|
||||
for( var tmp = lhs; tmp < end; ++tmp, ++rhs )
|
||||
{
|
||||
if( *lhs == 0 )
|
||||
{
|
||||
return *rhs == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
if( *rhs == 0 )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var diff = *tmp - *rhs;
|
||||
if( diff != 0 )
|
||||
{
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check two byte arrays of given length for equality.
|
||||
public static bool Equals( byte* lhs, int lhsLength, byte* rhs, int rhsLength )
|
||||
{
|
||||
if( lhsLength != rhsLength )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( lhs == rhs || lhsLength == 0 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Functions.MemCmpUnchecked( lhs, rhs, lhsLength ) == 0;
|
||||
}
|
||||
|
||||
// Check one byte array of given length for equality against a null-terminated byte array of unknown length.
|
||||
private static bool Equal( byte* lhs, int lhsLength, byte* rhs )
|
||||
=> Compare( lhs, lhsLength, rhs ) == 0;
|
||||
|
||||
// Check two null-terminated byte arrays of unknown length not larger than maxLength for equality.
|
||||
private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue )
|
||||
=> Compare( lhs, rhs, maxLength ) == 0;
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
public static unsafe partial class ByteStringFunctions
|
||||
{
|
||||
// Used for static null-terminators.
|
||||
public class NullTerminator
|
||||
{
|
||||
public readonly byte* NullBytePtr;
|
||||
|
||||
public NullTerminator()
|
||||
{
|
||||
NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 );
|
||||
*NullBytePtr = 0;
|
||||
}
|
||||
|
||||
~NullTerminator()
|
||||
=> Marshal.FreeHGlobal( ( IntPtr )NullBytePtr );
|
||||
}
|
||||
|
||||
// Convert a C# unicode-string to an unmanaged UTF8-byte array and return the pointer.
|
||||
// If the length would exceed the given maxLength, return a nullpointer instead.
|
||||
public static byte* Utf8FromString( string s, out int length, int maxLength = int.MaxValue )
|
||||
{
|
||||
length = Encoding.UTF8.GetByteCount( s );
|
||||
if( length >= maxLength )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = ( byte* )Marshal.AllocHGlobal( length + 1 );
|
||||
fixed( char* ptr = s )
|
||||
{
|
||||
Encoding.UTF8.GetBytes( ptr, length, path, length + 1 );
|
||||
}
|
||||
|
||||
path[ length ] = 0;
|
||||
return path;
|
||||
}
|
||||
|
||||
// Create a copy of a given string and return the pointer.
|
||||
public static byte* CopyString( byte* path, int length )
|
||||
{
|
||||
var ret = ( byte* )Marshal.AllocHGlobal( length + 1 );
|
||||
Functions.MemCpyUnchecked( ret, path, length );
|
||||
ret[ length ] = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Check the length of a null-terminated byte array no longer than the given maxLength.
|
||||
public static int CheckLength( byte* path, int maxLength = int.MaxValue )
|
||||
{
|
||||
var end = path + maxLength;
|
||||
for( var it = path; it < end; ++it )
|
||||
{
|
||||
if( *it == 0 )
|
||||
{
|
||||
return ( int )( it - path );
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException( "Null-terminated path too long" );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
public static unsafe partial class ByteStringFunctions
|
||||
{
|
||||
// Replace all occurrences of from in a byte array of known length with to.
|
||||
public static int Replace( byte* ptr, int length, byte from, byte to )
|
||||
{
|
||||
var end = ptr + length;
|
||||
var numReplaced = 0;
|
||||
for( ; ptr < end; ++ptr )
|
||||
{
|
||||
if( *ptr == from )
|
||||
{
|
||||
*ptr = to;
|
||||
++numReplaced;
|
||||
}
|
||||
}
|
||||
|
||||
return numReplaced;
|
||||
}
|
||||
|
||||
// Convert a byte array of given length to ASCII-lowercase.
|
||||
public static void AsciiToLowerInPlace( byte* path, int length )
|
||||
{
|
||||
for( var i = 0; i < length; ++i )
|
||||
{
|
||||
path[ i ] = AsciiLowerCaseBytes[ path[ i ] ];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy a byte array and convert the copy to ASCII-lowercase.
|
||||
public static byte* AsciiToLower( byte* path, int length )
|
||||
{
|
||||
var ptr = ( byte* )Marshal.AllocHGlobal( length + 1 );
|
||||
ptr[ length ] = 0;
|
||||
for( var i = 0; i < length; ++i )
|
||||
{
|
||||
ptr[ i ] = AsciiLowerCaseBytes[ path[ i ] ];
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
[JsonConverter( typeof( FullPathConverter ) )]
|
||||
public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||
{
|
||||
public readonly string FullName;
|
||||
public readonly Utf8String InternalName;
|
||||
public readonly ulong Crc64;
|
||||
|
||||
public static readonly FullPath Empty = new(string.Empty);
|
||||
|
||||
public FullPath( DirectoryInfo baseDir, Utf8RelPath relPath )
|
||||
: this( Path.Combine( baseDir.FullName, relPath.ToString() ) )
|
||||
{ }
|
||||
|
||||
public FullPath( FileInfo file )
|
||||
: this( file.FullName )
|
||||
{ }
|
||||
|
||||
|
||||
public FullPath( string s )
|
||||
{
|
||||
FullName = s;
|
||||
InternalName = Utf8String.FromString( FullName.Replace( '\\', '/' ), out var name, true ) ? name : Utf8String.Empty;
|
||||
Crc64 = Functions.ComputeCrc64( InternalName.Span );
|
||||
}
|
||||
|
||||
public bool Exists
|
||||
=> File.Exists( FullName );
|
||||
|
||||
public string Extension
|
||||
=> Path.GetExtension( FullName );
|
||||
|
||||
public string Name
|
||||
=> Path.GetFileName( FullName );
|
||||
|
||||
public bool ToGamePath( DirectoryInfo dir, out Utf8GamePath path )
|
||||
{
|
||||
path = Utf8GamePath.Empty;
|
||||
if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
||||
|
||||
path = new Utf8GamePath( substring );
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ToRelPath( DirectoryInfo dir, out Utf8RelPath path )
|
||||
{
|
||||
path = Utf8RelPath.Empty;
|
||||
if( !FullName.StartsWith( dir.FullName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
||||
|
||||
path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
public int CompareTo( object? obj )
|
||||
=> obj switch
|
||||
{
|
||||
FullPath p => InternalName?.CompareTo( p.InternalName ) ?? -1,
|
||||
FileInfo f => string.Compare( FullName, f.FullName, StringComparison.OrdinalIgnoreCase ),
|
||||
Utf8String u => InternalName?.CompareTo( u ) ?? -1,
|
||||
string s => string.Compare( FullName, s, StringComparison.OrdinalIgnoreCase ),
|
||||
_ => -1,
|
||||
};
|
||||
|
||||
public bool Equals( FullPath other )
|
||||
{
|
||||
if( Crc64 != other.Crc64 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( FullName.Length == 0 || other.FullName.Length == 0 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return InternalName.Equals( other.InternalName );
|
||||
}
|
||||
|
||||
public bool IsRooted
|
||||
=> new Utf8GamePath( InternalName ).IsRooted();
|
||||
|
||||
public override int GetHashCode()
|
||||
=> InternalName.Crc32;
|
||||
|
||||
public override string ToString()
|
||||
=> FullName;
|
||||
|
||||
public class FullPathConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( FullPath );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
var token = JToken.Load( reader ).ToString();
|
||||
return new FullPath( token );
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
=> true;
|
||||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
if( value is FullPath p )
|
||||
{
|
||||
serializer.Serialize( writer, p.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
// NewGamePath wrap some additional validity checking around Utf8String,
|
||||
// provide some filesystem helpers, and conversion to Json.
|
||||
[JsonConverter( typeof( Utf8GamePathConverter ) )]
|
||||
public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable
|
||||
{
|
||||
public const int MaxGamePathLength = 256;
|
||||
|
||||
public readonly Utf8String Path;
|
||||
public static readonly Utf8GamePath Empty = new(Utf8String.Empty);
|
||||
|
||||
internal Utf8GamePath( Utf8String s )
|
||||
=> Path = s;
|
||||
|
||||
public int Length
|
||||
=> Path.Length;
|
||||
|
||||
public bool IsEmpty
|
||||
=> Path.IsEmpty;
|
||||
|
||||
public Utf8GamePath ToLower()
|
||||
=> new(Path.AsciiToLower());
|
||||
|
||||
public static unsafe bool FromPointer( byte* ptr, out Utf8GamePath path, bool lower = false )
|
||||
{
|
||||
var utf = new Utf8String( ptr );
|
||||
return ReturnChecked( utf, out path, lower );
|
||||
}
|
||||
|
||||
public static bool FromSpan( ReadOnlySpan< byte > data, out Utf8GamePath path, bool lower = false )
|
||||
{
|
||||
var utf = Utf8String.FromSpanUnsafe( data, false, null, null );
|
||||
return ReturnChecked( utf, out path, lower );
|
||||
}
|
||||
|
||||
// Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one.
|
||||
// Does not check for initial slashes either, since they are assumed to be by choice.
|
||||
// Checks for maxlength, ASCII and lowercase.
|
||||
private static bool ReturnChecked( Utf8String utf, out Utf8GamePath path, bool lower = false )
|
||||
{
|
||||
path = Empty;
|
||||
if( !utf.IsAscii || utf.Length > MaxGamePathLength )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf );
|
||||
return true;
|
||||
}
|
||||
|
||||
public Utf8GamePath Clone()
|
||||
=> new(Path.Clone());
|
||||
|
||||
public static explicit operator Utf8GamePath( string s )
|
||||
=> FromString( s, out var p, true ) ? p : Empty;
|
||||
|
||||
public static bool FromString( string? s, out Utf8GamePath path, bool toLower = false )
|
||||
{
|
||||
path = Empty;
|
||||
if( s.IsNullOrEmpty() )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var substring = s!.Replace( '\\', '/' ).TrimStart( '/' );
|
||||
if( substring.Length > MaxGamePathLength )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( substring.Length == 0 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if( !Utf8String.FromString( substring, out var ascii, toLower ) || !ascii.IsAscii )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path = new Utf8GamePath( ascii );
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8GamePath path, bool toLower = false )
|
||||
{
|
||||
path = Empty;
|
||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var substring = file.FullName[ ( baseDir.FullName.Length + 1 ).. ];
|
||||
return FromString( substring, out path, toLower );
|
||||
}
|
||||
|
||||
public Utf8String Filename()
|
||||
{
|
||||
var idx = Path.LastIndexOf( ( byte )'/' );
|
||||
return idx == -1 ? Path : Path.Substring( idx + 1 );
|
||||
}
|
||||
|
||||
public Utf8String Extension()
|
||||
{
|
||||
var idx = Path.LastIndexOf( ( byte )'.' );
|
||||
return idx == -1 ? Utf8String.Empty : Path.Substring( idx );
|
||||
}
|
||||
|
||||
public bool Equals( Utf8GamePath other )
|
||||
=> Path.Equals( other.Path );
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Path.GetHashCode();
|
||||
|
||||
public int CompareTo( Utf8GamePath other )
|
||||
=> Path.CompareTo( other.Path );
|
||||
|
||||
public override string ToString()
|
||||
=> Path.ToString();
|
||||
|
||||
public void Dispose()
|
||||
=> Path.Dispose();
|
||||
|
||||
public bool IsRooted()
|
||||
=> IsRooted( Path );
|
||||
|
||||
public static bool IsRooted( Utf8String path )
|
||||
=> path.Length >= 1 && ( path[ 0 ] == '/' || path[ 0 ] == '\\' )
|
||||
|| path.Length >= 2
|
||||
&& ( path[ 0 ] >= 'A' && path[ 0 ] <= 'Z' || path[ 0 ] >= 'a' && path[ 0 ] <= 'z' )
|
||||
&& path[ 1 ] == ':';
|
||||
|
||||
public class Utf8GamePathConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( Utf8GamePath );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
var token = JToken.Load( reader ).ToString();
|
||||
return FromString( token, out var p, true )
|
||||
? p
|
||||
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8GamePath )}." );
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
=> true;
|
||||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
if( value is Utf8GamePath p )
|
||||
{
|
||||
serializer.Serialize( writer, p.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GamePath ToGamePath()
|
||||
=> GamePath.GenerateUnchecked( ToString() );
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
[JsonConverter( typeof( Utf8RelPathConverter ) )]
|
||||
public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable
|
||||
{
|
||||
public const int MaxRelPathLength = 250;
|
||||
|
||||
public readonly Utf8String Path;
|
||||
public static readonly Utf8RelPath Empty = new(Utf8String.Empty);
|
||||
|
||||
internal Utf8RelPath( Utf8String path )
|
||||
=> Path = path;
|
||||
|
||||
|
||||
public static explicit operator Utf8RelPath( string s )
|
||||
{
|
||||
if( !FromString( s, out var p ) )
|
||||
{
|
||||
return Empty;
|
||||
}
|
||||
|
||||
return new Utf8RelPath( p.Path.AsciiToLower() );
|
||||
}
|
||||
|
||||
public static bool FromString( string? s, out Utf8RelPath path )
|
||||
{
|
||||
path = Empty;
|
||||
if( s.IsNullOrEmpty() )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var substring = s!.Replace( '/', '\\' ).TrimStart('\\');
|
||||
if( substring.Length > MaxRelPathLength )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( substring.Length == 0 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if( !Utf8String.FromString( substring, out var ascii, true ) || !ascii.IsAscii )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path = new Utf8RelPath( ascii );
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8RelPath path )
|
||||
{
|
||||
path = Empty;
|
||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var substring = file.FullName[ (baseDir.FullName.Length + 1).. ];
|
||||
return FromString( substring, out path );
|
||||
}
|
||||
|
||||
public static bool FromFile( FullPath file, DirectoryInfo baseDir, out Utf8RelPath path )
|
||||
{
|
||||
path = Empty;
|
||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var substring = file.FullName[ (baseDir.FullName.Length + 1).. ];
|
||||
return FromString( substring, out path );
|
||||
}
|
||||
|
||||
public Utf8RelPath( Utf8GamePath gamePath )
|
||||
=> Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' );
|
||||
|
||||
public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 )
|
||||
{
|
||||
var idx = 0;
|
||||
while( skipFolders > 0 )
|
||||
{
|
||||
idx = Path.IndexOf( ( byte )'\\', idx ) + 1;
|
||||
--skipFolders;
|
||||
if( idx <= 0 )
|
||||
{
|
||||
return Utf8GamePath.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
var length = Path.Length - idx;
|
||||
var ptr = ByteStringFunctions.CopyString( Path.Path + idx, length );
|
||||
ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' );
|
||||
ByteStringFunctions.AsciiToLowerInPlace( ptr, length );
|
||||
var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true );
|
||||
return new Utf8GamePath( utf );
|
||||
}
|
||||
|
||||
public int CompareTo( Utf8RelPath rhs )
|
||||
=> Path.CompareTo( rhs.Path );
|
||||
|
||||
public bool Equals( Utf8RelPath other )
|
||||
=> Path.Equals( other.Path );
|
||||
|
||||
public override string ToString()
|
||||
=> Path.ToString();
|
||||
|
||||
public void Dispose()
|
||||
=> Path.Dispose();
|
||||
|
||||
public class Utf8RelPathConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( Utf8RelPath );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
var token = JToken.Load( reader ).ToString();
|
||||
return FromString( token, out var p )
|
||||
? p
|
||||
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8RelPath )}." );
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
=> true;
|
||||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
if( value is Utf8RelPath p )
|
||||
{
|
||||
serializer.Serialize( writer, p.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
// Utf8String is a wrapper around unsafe byte strings.
|
||||
// It may be used to store owned strings in unmanaged space,
|
||||
// as well as refer to unowned strings.
|
||||
// Unowned strings may change their value and thus become corrupt,
|
||||
// so they should never be stored, just used locally or with great care.
|
||||
// The string keeps track of whether it is owned or not, it also can keep track
|
||||
// of some other information, like the string being pure ASCII, ASCII-lowercase or null-terminated.
|
||||
// Owned strings are always null-terminated.
|
||||
// Any constructed string will compute its own CRC32-value (as long as the string itself is not changed).
|
||||
public sealed unsafe partial class Utf8String : IEnumerable< byte >
|
||||
{
|
||||
// We keep information on some of the state of the Utf8String in specific bits.
|
||||
// This costs some potential max size, but that is not relevant for our case.
|
||||
// Except for destruction/dispose, or if the non-owned pointer changes values,
|
||||
// the CheckedFlag, AsciiLowerCaseFlag and AsciiFlag are the only things that are mutable.
|
||||
private const uint NullTerminatedFlag = 0x80000000;
|
||||
private const uint OwnedFlag = 0x40000000;
|
||||
private const uint AsciiCheckedFlag = 0x04000000;
|
||||
private const uint AsciiFlag = 0x08000000;
|
||||
private const uint AsciiLowerCheckedFlag = 0x10000000;
|
||||
private const uint AsciiLowerFlag = 0x20000000;
|
||||
private const uint FlagMask = 0x03FFFFFF;
|
||||
|
||||
public bool IsNullTerminated
|
||||
=> ( _length & NullTerminatedFlag ) != 0;
|
||||
|
||||
public bool IsOwned
|
||||
=> ( _length & OwnedFlag ) != 0;
|
||||
|
||||
public bool IsAscii
|
||||
=> CheckAscii();
|
||||
|
||||
public bool IsAsciiLowerCase
|
||||
=> CheckAsciiLower();
|
||||
|
||||
public byte* Path
|
||||
=> _path;
|
||||
|
||||
public int Crc32
|
||||
=> _crc32;
|
||||
|
||||
public int Length
|
||||
=> ( int )( _length & FlagMask );
|
||||
|
||||
public bool IsEmpty
|
||||
=> Length == 0;
|
||||
|
||||
public ReadOnlySpan< byte > Span
|
||||
=> new(_path, Length);
|
||||
|
||||
public byte this[ int idx ]
|
||||
=> ( uint )idx < Length ? _path[ idx ] : throw new IndexOutOfRangeException();
|
||||
|
||||
public IEnumerator< byte > GetEnumerator()
|
||||
{
|
||||
for( var i = 0; i < Length; ++i )
|
||||
{
|
||||
yield return Span[ i ];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
// Only not readonly due to dispose.
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
public override int GetHashCode()
|
||||
=> _crc32;
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, IComparable< Utf8String >
|
||||
{
|
||||
public bool Equals( Utf8String? other )
|
||||
{
|
||||
if( ReferenceEquals( null, other ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( ReferenceEquals( this, other ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length );
|
||||
}
|
||||
|
||||
public bool EqualsCi( Utf8String? other )
|
||||
{
|
||||
if( ReferenceEquals( null, other ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( ReferenceEquals( this, other ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) )
|
||||
{
|
||||
return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length );
|
||||
}
|
||||
|
||||
return ByteStringFunctions.AsciiCaselessEquals( _path, Length, other._path, other.Length );
|
||||
}
|
||||
|
||||
public int CompareTo( Utf8String? other )
|
||||
{
|
||||
if( ReferenceEquals( this, other ) )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( ReferenceEquals( null, other ) )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ByteStringFunctions.Compare( _path, Length, other._path, other.Length );
|
||||
}
|
||||
|
||||
public int CompareToCi( Utf8String? other )
|
||||
{
|
||||
if( ReferenceEquals( null, other ) )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( ReferenceEquals( this, other ) )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) )
|
||||
{
|
||||
return ByteStringFunctions.Compare( _path, Length, other._path, other.Length );
|
||||
}
|
||||
|
||||
return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length );
|
||||
}
|
||||
|
||||
public bool StartsWith( Utf8String other )
|
||||
{
|
||||
var otherLength = other.Length;
|
||||
return otherLength <= Length && ByteStringFunctions.Equals( other.Path, otherLength, Path, otherLength );
|
||||
}
|
||||
|
||||
public bool EndsWith( Utf8String other )
|
||||
{
|
||||
var otherLength = other.Length;
|
||||
var offset = Length - otherLength;
|
||||
return offset >= 0 && ByteStringFunctions.Equals( other.Path, otherLength, Path + offset, otherLength );
|
||||
}
|
||||
|
||||
public bool StartsWith( params char[] chars )
|
||||
{
|
||||
if( chars.Length > Length )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ptr = _path;
|
||||
return chars.All( t => *ptr++ == ( byte )t );
|
||||
}
|
||||
|
||||
public bool EndsWith( params char[] chars )
|
||||
{
|
||||
if( chars.Length > Length )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ptr = _path + Length - chars.Length;
|
||||
return chars.All( c => *ptr++ == ( byte )c );
|
||||
}
|
||||
|
||||
public int IndexOf( byte b, int from = 0 )
|
||||
{
|
||||
var end = _path + Length;
|
||||
for( var tmp = _path + from; tmp < end; ++tmp )
|
||||
{
|
||||
if( *tmp == b )
|
||||
{
|
||||
return ( int )( tmp - _path );
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int LastIndexOf( byte b, int to = 0 )
|
||||
{
|
||||
var end = _path + to;
|
||||
for( var tmp = _path + Length - 1; tmp >= end; --tmp )
|
||||
{
|
||||
if( *tmp == b )
|
||||
{
|
||||
return ( int )( tmp - _path );
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
public sealed unsafe partial class Utf8String : IDisposable
|
||||
{
|
||||
// statically allocated null-terminator for empty strings to point to.
|
||||
private static readonly ByteStringFunctions.NullTerminator Null = new();
|
||||
|
||||
public static readonly Utf8String Empty = new();
|
||||
|
||||
// actual data members.
|
||||
private byte* _path;
|
||||
private uint _length;
|
||||
private int _crc32;
|
||||
|
||||
// Create an empty string.
|
||||
public Utf8String()
|
||||
{
|
||||
_path = Null.NullBytePtr;
|
||||
_length |= AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag | AsciiFlag;
|
||||
_crc32 = 0;
|
||||
}
|
||||
|
||||
// Create a temporary Utf8String from a byte pointer.
|
||||
// This computes CRC, checks for ASCII and AsciiLower and assumes Null-Termination.
|
||||
public Utf8String( byte* path )
|
||||
{
|
||||
var length = Functions.ComputeCrc32AsciiLowerAndSize( path, out var crc32, out var lower, out var ascii );
|
||||
Setup( path, length, crc32, true, false, lower, ascii );
|
||||
}
|
||||
|
||||
// Construct a temporary Utf8String from a given byte string of known size.
|
||||
// Other known attributes can also be provided and are not computed.
|
||||
// Can throw ArgumentOutOfRange if length is higher than max length.
|
||||
// The Crc32 will be computed.
|
||||
public static Utf8String FromByteStringUnsafe( byte* path, int length, bool isNullTerminated, bool? isLower = null, bool? isAscii = false )
|
||||
=> new Utf8String().Setup( path, length, null, isNullTerminated, false, isLower, isAscii );
|
||||
|
||||
// Same as above, just with a span.
|
||||
public static Utf8String FromSpanUnsafe( ReadOnlySpan< byte > path, bool isNullTerminated, bool? isLower = null, bool? isAscii = false )
|
||||
{
|
||||
fixed( byte* ptr = path )
|
||||
{
|
||||
return FromByteStringUnsafe( ptr, path.Length, isNullTerminated, isLower, isAscii );
|
||||
}
|
||||
}
|
||||
|
||||
// Construct a Utf8String from a given unicode string, possibly converted to ascii lowercase.
|
||||
// Only returns false if the length exceeds the max length.
|
||||
public static bool FromString( string? path, out Utf8String ret, bool toAsciiLower = false )
|
||||
{
|
||||
if( string.IsNullOrEmpty( path ) )
|
||||
{
|
||||
ret = Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
var p = ByteStringFunctions.Utf8FromString( path, out var l, ( int )FlagMask );
|
||||
if( p == null )
|
||||
{
|
||||
ret = Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( toAsciiLower )
|
||||
{
|
||||
ByteStringFunctions.AsciiToLowerInPlace( p, l );
|
||||
}
|
||||
|
||||
ret = new Utf8String().Setup( p, l, null, true, true, toAsciiLower ? true : null, l == path.Length );
|
||||
return true;
|
||||
}
|
||||
|
||||
// Does not check for length and just assumes the isLower state from the second argument.
|
||||
public static Utf8String FromStringUnsafe( string? path, bool? isLower )
|
||||
{
|
||||
if( string.IsNullOrEmpty( path ) )
|
||||
{
|
||||
return Empty;
|
||||
}
|
||||
|
||||
var p = ByteStringFunctions.Utf8FromString( path, out var l );
|
||||
var ret = new Utf8String().Setup( p, l, null, true, true, isLower, l == path.Length );
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Free memory if the string is owned.
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
if( !IsOwned )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal( ( IntPtr )_path );
|
||||
GC.RemoveMemoryPressure( Length + 1 );
|
||||
_length = AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag;
|
||||
_path = Null.NullBytePtr;
|
||||
_crc32 = 0;
|
||||
}
|
||||
|
||||
// Manually free memory. Sets the string to an empty string.
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseUnmanagedResources();
|
||||
GC.SuppressFinalize( this );
|
||||
}
|
||||
|
||||
~Utf8String()
|
||||
{
|
||||
ReleaseUnmanagedResources();
|
||||
}
|
||||
|
||||
// Setup from all given values.
|
||||
// Only called from constructors or factory functions in this library.
|
||||
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
|
||||
internal Utf8String Setup( byte* path, int length, int? crc32, bool isNullTerminated, bool isOwned,
|
||||
bool? isLower = null, bool? isAscii = null )
|
||||
{
|
||||
if( length > FlagMask )
|
||||
{
|
||||
throw new ArgumentOutOfRangeException( nameof( length ) );
|
||||
}
|
||||
|
||||
_path = path;
|
||||
_length = ( uint )length;
|
||||
_crc32 = crc32 ?? ( int )~Lumina.Misc.Crc32.Get( new ReadOnlySpan< byte >( path, length ) );
|
||||
if( isNullTerminated )
|
||||
{
|
||||
_length |= NullTerminatedFlag;
|
||||
}
|
||||
|
||||
if( isOwned )
|
||||
{
|
||||
GC.AddMemoryPressure( length + 1 );
|
||||
_length |= OwnedFlag;
|
||||
}
|
||||
|
||||
if( isLower != null )
|
||||
{
|
||||
_length |= AsciiLowerCheckedFlag;
|
||||
if( isLower.Value )
|
||||
{
|
||||
_length |= AsciiLowerFlag;
|
||||
}
|
||||
}
|
||||
|
||||
if( isAscii != null )
|
||||
{
|
||||
_length |= AsciiCheckedFlag;
|
||||
if( isAscii.Value )
|
||||
{
|
||||
_length |= AsciiFlag;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private bool CheckAscii()
|
||||
{
|
||||
switch( _length & ( AsciiCheckedFlag | AsciiFlag ) )
|
||||
{
|
||||
case AsciiCheckedFlag: return false;
|
||||
case AsciiCheckedFlag | AsciiFlag: return true;
|
||||
default:
|
||||
_length |= AsciiCheckedFlag;
|
||||
var isAscii = ByteStringFunctions.IsAscii( _path, Length );
|
||||
if( isAscii )
|
||||
{
|
||||
_length |= AsciiFlag;
|
||||
}
|
||||
|
||||
return isAscii;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckAsciiLower()
|
||||
{
|
||||
switch( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) )
|
||||
{
|
||||
case AsciiLowerCheckedFlag: return false;
|
||||
case AsciiLowerCheckedFlag | AsciiLowerFlag: return true;
|
||||
default:
|
||||
_length |= AsciiLowerCheckedFlag;
|
||||
var isAsciiLower = ByteStringFunctions.IsAsciiLowerCase( _path, Length );
|
||||
if( isAsciiLower )
|
||||
{
|
||||
_length |= AsciiLowerFlag;
|
||||
}
|
||||
|
||||
return isAsciiLower;
|
||||
}
|
||||
}
|
||||
|
||||
private bool? IsAsciiInternal
|
||||
=> ( _length & ( AsciiCheckedFlag | AsciiFlag ) ) switch
|
||||
{
|
||||
AsciiCheckedFlag => false,
|
||||
AsciiFlag => true,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private bool? IsAsciiLowerInternal
|
||||
=> ( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) switch
|
||||
{
|
||||
AsciiLowerCheckedFlag => false,
|
||||
AsciiLowerFlag => true,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
public sealed unsafe partial class Utf8String
|
||||
{
|
||||
// Create a C# Unicode string from this string.
|
||||
// If the string is known to be pure ASCII, use that encoding, otherwise UTF8.
|
||||
public override string ToString()
|
||||
=> Length == 0
|
||||
? string.Empty
|
||||
: ( _length & AsciiFlag ) != 0
|
||||
? Encoding.ASCII.GetString( _path, Length )
|
||||
: Encoding.UTF8.GetString( _path, Length );
|
||||
|
||||
|
||||
// Convert the ascii portion of the string to lowercase.
|
||||
// Only creates a new string and copy if the string is not already known to be lowercase.
|
||||
public Utf8String AsciiToLower()
|
||||
=> ( _length & AsciiLowerFlag ) == 0
|
||||
? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal )
|
||||
: this;
|
||||
|
||||
// Convert the ascii portion of the string to lowercase.
|
||||
// Guaranteed to create an owned copy.
|
||||
public Utf8String AsciiToLowerClone()
|
||||
=> ( _length & AsciiLowerFlag ) == 0
|
||||
? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal )
|
||||
: Clone();
|
||||
|
||||
// Create an owned copy of the given string.
|
||||
public Utf8String Clone()
|
||||
{
|
||||
var ret = new Utf8String();
|
||||
ret._length = _length | OwnedFlag | NullTerminatedFlag;
|
||||
ret._path = ByteStringFunctions.CopyString(Path, Length);
|
||||
ret._crc32 = Crc32;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Create a non-owning substring from the given position.
|
||||
// If from is negative or too large, the returned string will be the empty string.
|
||||
public Utf8String Substring( int from )
|
||||
=> ( uint )from < Length
|
||||
? FromByteStringUnsafe( _path + from, Length - from, IsNullTerminated, IsAsciiLowerInternal, IsAsciiInternal )
|
||||
: Empty;
|
||||
|
||||
// Create a non-owning substring from the given position of the given length.
|
||||
// If from is negative or too large, the returned string will be the empty string.
|
||||
// If from + length is too large, it will be the same as if length was not specified.
|
||||
public Utf8String Substring( int from, int length )
|
||||
{
|
||||
var maxLength = Length - ( uint )from;
|
||||
if( maxLength <= 0 )
|
||||
{
|
||||
return Empty;
|
||||
}
|
||||
|
||||
return length < maxLength
|
||||
? FromByteStringUnsafe( _path + from, length, false, IsAsciiLowerInternal, IsAsciiInternal )
|
||||
: Substring( from );
|
||||
}
|
||||
|
||||
// Create a owned copy of the string and replace all occurences of from with to in it.
|
||||
public Utf8String Replace( byte from, byte to )
|
||||
{
|
||||
var length = Length;
|
||||
var newPtr = ByteStringFunctions.CopyString( _path, length );
|
||||
var numReplaced = ByteStringFunctions.Replace( newPtr, length, from, to );
|
||||
return new Utf8String().Setup( newPtr, length, numReplaced == 0 ? _crc32 : null, true, true, IsAsciiLowerInternal, IsAsciiInternal );
|
||||
}
|
||||
|
||||
// Join a number of strings with a given byte between them.
|
||||
public static Utf8String Join( byte splitter, params Utf8String[] strings )
|
||||
{
|
||||
var length = strings.Sum( s => s.Length ) + strings.Length;
|
||||
var data = ( byte* )Marshal.AllocHGlobal( length );
|
||||
|
||||
var ptr = data;
|
||||
bool? isLower = ByteStringFunctions.AsciiIsLower( splitter );
|
||||
bool? isAscii = splitter < 128;
|
||||
foreach( var s in strings )
|
||||
{
|
||||
Functions.MemCpyUnchecked( ptr, s.Path, s.Length );
|
||||
ptr += s.Length;
|
||||
*ptr++ = splitter;
|
||||
isLower = Combine( isLower, s.IsAsciiLowerInternal );
|
||||
isAscii &= s.IsAscii;
|
||||
}
|
||||
|
||||
--length;
|
||||
data[ length ] = 0;
|
||||
var ret = FromByteStringUnsafe( data, length, true, isLower, isAscii );
|
||||
ret._length |= OwnedFlag;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Split a string and return a list of the substrings delimited by b.
|
||||
// You can specify the maximum number of splits (if the maximum is reached, the last substring may contain delimiters).
|
||||
// You can also specify to ignore empty substrings inside delimiters. Those are also not counted for max splits.
|
||||
public List< Utf8String > Split( byte b, int maxSplits = int.MaxValue, bool removeEmpty = true )
|
||||
{
|
||||
var ret = new List< Utf8String >();
|
||||
var start = 0;
|
||||
for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) )
|
||||
{
|
||||
if( start != idx || !removeEmpty )
|
||||
{
|
||||
ret.Add( Substring( start, idx - start ) );
|
||||
}
|
||||
|
||||
start = idx + 1;
|
||||
if( ret.Count == maxSplits - 1 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ret.Add( Substring( start ) );
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool? Combine( bool? val1, bool? val2 )
|
||||
{
|
||||
return ( val1, val2 ) switch
|
||||
{
|
||||
(null, null) => null,
|
||||
(null, true) => null,
|
||||
(null, false) => false,
|
||||
(true, null) => null,
|
||||
(true, true) => true,
|
||||
(true, false) => false,
|
||||
(false, null) => false,
|
||||
(false, true) => false,
|
||||
(false, false) => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Penumbra.GameData.Enums;
|
||||
|
||||
public enum BodySlot : byte
|
||||
{
|
||||
Unknown,
|
||||
Hair,
|
||||
Face,
|
||||
Tail,
|
||||
Body,
|
||||
Zear,
|
||||
}
|
||||
|
||||
public static class BodySlotEnumExtension
|
||||
{
|
||||
public static string ToSuffix( this BodySlot value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
BodySlot.Zear => "zear",
|
||||
BodySlot.Face => "face",
|
||||
BodySlot.Hair => "hair",
|
||||
BodySlot.Body => "body",
|
||||
BodySlot.Tail => "tail",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class Names
|
||||
{
|
||||
public static readonly Dictionary< string, BodySlot > StringToBodySlot = new()
|
||||
{
|
||||
{ BodySlot.Zear.ToSuffix(), BodySlot.Zear },
|
||||
{ BodySlot.Face.ToSuffix(), BodySlot.Face },
|
||||
{ BodySlot.Hair.ToSuffix(), BodySlot.Hair },
|
||||
{ BodySlot.Body.ToSuffix(), BodySlot.Body },
|
||||
{ BodySlot.Tail.ToSuffix(), BodySlot.Tail },
|
||||
};
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
using System;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Action = Lumina.Excel.GeneratedSheets.Action;
|
||||
|
||||
namespace Penumbra.GameData.Enums
|
||||
{
|
||||
public enum ChangedItemType
|
||||
{
|
||||
None,
|
||||
Item,
|
||||
Action,
|
||||
Customization,
|
||||
}
|
||||
|
||||
public static class ChangedItemExtensions
|
||||
{
|
||||
public static (ChangedItemType, uint) ChangedItemToTypeAndId( object? item )
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
null => ( ChangedItemType.None, 0 ),
|
||||
Item i => ( ChangedItemType.Item, i.RowId ),
|
||||
Action a => ( ChangedItemType.Action, a.RowId ),
|
||||
_ => ( ChangedItemType.Customization, 0 ),
|
||||
};
|
||||
}
|
||||
|
||||
public static object? GetObject( this ChangedItemType type, uint id )
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ChangedItemType.None => null,
|
||||
ChangedItemType.Item => ObjectIdentification.DataManager?.GetExcelSheet< Item >()?.GetRow( id ),
|
||||
ChangedItemType.Action => ObjectIdentification.DataManager?.GetExcelSheet< Action >()?.GetRow( id ),
|
||||
ChangedItemType.Customization => null,
|
||||
_ => throw new ArgumentOutOfRangeException( nameof( type ), type, null )
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Penumbra.GameData.Enums
|
||||
{
|
||||
public enum CustomizationType : byte
|
||||
{
|
||||
Unknown,
|
||||
Body,
|
||||
Tail,
|
||||
Face,
|
||||
Iris,
|
||||
Accessory,
|
||||
Hair,
|
||||
Zear,
|
||||
DecalFace,
|
||||
DecalEquip,
|
||||
Skin,
|
||||
Etc,
|
||||
}
|
||||
|
||||
public static class CustomizationTypeEnumExtension
|
||||
{
|
||||
public static string ToSuffix( this CustomizationType value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
CustomizationType.Body => "top",
|
||||
CustomizationType.Face => "fac",
|
||||
CustomizationType.Iris => "iri",
|
||||
CustomizationType.Accessory => "acc",
|
||||
CustomizationType.Hair => "hir",
|
||||
CustomizationType.Tail => "til",
|
||||
CustomizationType.Zear => "zer",
|
||||
CustomizationType.Etc => "etc",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class Names
|
||||
{
|
||||
public static readonly Dictionary< string, CustomizationType > SuffixToCustomizationType = new()
|
||||
{
|
||||
{ CustomizationType.Body.ToSuffix(), CustomizationType.Body },
|
||||
{ CustomizationType.Face.ToSuffix(), CustomizationType.Face },
|
||||
{ CustomizationType.Iris.ToSuffix(), CustomizationType.Iris },
|
||||
{ CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory },
|
||||
{ CustomizationType.Hair.ToSuffix(), CustomizationType.Hair },
|
||||
{ CustomizationType.Tail.ToSuffix(), CustomizationType.Tail },
|
||||
{ CustomizationType.Zear.ToSuffix(), CustomizationType.Zear },
|
||||
{ CustomizationType.Etc.ToSuffix(), CustomizationType.Etc },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
|
||||
namespace Penumbra.GameData.Enums;
|
||||
|
||||
public enum EquipSlot : byte
|
||||
{
|
||||
Unknown = 0,
|
||||
MainHand = 1,
|
||||
OffHand = 2,
|
||||
Head = 3,
|
||||
Body = 4,
|
||||
Hands = 5,
|
||||
Belt = 6,
|
||||
Legs = 7,
|
||||
Feet = 8,
|
||||
Ears = 9,
|
||||
Neck = 10,
|
||||
Wrists = 11,
|
||||
RFinger = 12,
|
||||
BothHand = 13,
|
||||
LFinger = 14, // Not officially existing, means "weapon could be equipped in either hand" for the game.
|
||||
HeadBody = 15,
|
||||
BodyHandsLegsFeet = 16,
|
||||
SoulCrystal = 17,
|
||||
LegsFeet = 18,
|
||||
FullBody = 19,
|
||||
BodyHands = 20,
|
||||
BodyLegsFeet = 21,
|
||||
All = 22, // Not officially existing
|
||||
}
|
||||
|
||||
public static class EquipSlotExtensions
|
||||
{
|
||||
public static EquipSlot ToEquipSlot( this uint value )
|
||||
=> value switch
|
||||
{
|
||||
0 => EquipSlot.Head,
|
||||
1 => EquipSlot.Body,
|
||||
2 => EquipSlot.Hands,
|
||||
3 => EquipSlot.Legs,
|
||||
4 => EquipSlot.Feet,
|
||||
5 => EquipSlot.Ears,
|
||||
6 => EquipSlot.Neck,
|
||||
7 => EquipSlot.Wrists,
|
||||
8 => EquipSlot.RFinger,
|
||||
9 => EquipSlot.LFinger,
|
||||
_ => EquipSlot.Unknown,
|
||||
};
|
||||
|
||||
public static string ToSuffix( this EquipSlot value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
EquipSlot.Head => "met",
|
||||
EquipSlot.Hands => "glv",
|
||||
EquipSlot.Legs => "dwn",
|
||||
EquipSlot.Feet => "sho",
|
||||
EquipSlot.Body => "top",
|
||||
EquipSlot.Ears => "ear",
|
||||
EquipSlot.Neck => "nek",
|
||||
EquipSlot.RFinger => "rir",
|
||||
EquipSlot.LFinger => "ril",
|
||||
EquipSlot.Wrists => "wrs",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static EquipSlot ToSlot( this EquipSlot value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
EquipSlot.MainHand => EquipSlot.MainHand,
|
||||
EquipSlot.OffHand => EquipSlot.OffHand,
|
||||
EquipSlot.Head => EquipSlot.Head,
|
||||
EquipSlot.Body => EquipSlot.Body,
|
||||
EquipSlot.Hands => EquipSlot.Hands,
|
||||
EquipSlot.Belt => EquipSlot.Belt,
|
||||
EquipSlot.Legs => EquipSlot.Legs,
|
||||
EquipSlot.Feet => EquipSlot.Feet,
|
||||
EquipSlot.Ears => EquipSlot.Ears,
|
||||
EquipSlot.Neck => EquipSlot.Neck,
|
||||
EquipSlot.Wrists => EquipSlot.Wrists,
|
||||
EquipSlot.RFinger => EquipSlot.RFinger,
|
||||
EquipSlot.BothHand => EquipSlot.MainHand,
|
||||
EquipSlot.LFinger => EquipSlot.RFinger,
|
||||
EquipSlot.HeadBody => EquipSlot.Body,
|
||||
EquipSlot.BodyHandsLegsFeet => EquipSlot.Body,
|
||||
EquipSlot.SoulCrystal => EquipSlot.SoulCrystal,
|
||||
EquipSlot.LegsFeet => EquipSlot.Legs,
|
||||
EquipSlot.FullBody => EquipSlot.Body,
|
||||
EquipSlot.BodyHands => EquipSlot.Body,
|
||||
EquipSlot.BodyLegsFeet => EquipSlot.Body,
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToName( this EquipSlot value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
EquipSlot.Head => "Head",
|
||||
EquipSlot.Hands => "Hands",
|
||||
EquipSlot.Legs => "Legs",
|
||||
EquipSlot.Feet => "Feet",
|
||||
EquipSlot.Body => "Body",
|
||||
EquipSlot.Ears => "Earrings",
|
||||
EquipSlot.Neck => "Necklace",
|
||||
EquipSlot.RFinger => "Right Ring",
|
||||
EquipSlot.LFinger => "Left Ring",
|
||||
EquipSlot.Wrists => "Bracelets",
|
||||
EquipSlot.MainHand => "Primary Weapon",
|
||||
EquipSlot.OffHand => "Secondary Weapon",
|
||||
EquipSlot.Belt => "Belt",
|
||||
EquipSlot.BothHand => "Primary Weapon",
|
||||
EquipSlot.HeadBody => "Head and Body",
|
||||
EquipSlot.BodyHandsLegsFeet => "Costume",
|
||||
EquipSlot.SoulCrystal => "Soul Crystal",
|
||||
EquipSlot.LegsFeet => "Bottom",
|
||||
EquipSlot.FullBody => "Costume",
|
||||
EquipSlot.BodyHands => "Top",
|
||||
EquipSlot.BodyLegsFeet => "Costume",
|
||||
EquipSlot.All => "Costume",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsEquipment( this EquipSlot value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
EquipSlot.Head => true,
|
||||
EquipSlot.Hands => true,
|
||||
EquipSlot.Legs => true,
|
||||
EquipSlot.Feet => true,
|
||||
EquipSlot.Body => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsAccessory( this EquipSlot value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
EquipSlot.Ears => true,
|
||||
EquipSlot.Neck => true,
|
||||
EquipSlot.RFinger => true,
|
||||
EquipSlot.LFinger => true,
|
||||
EquipSlot.Wrists => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues< EquipSlot >().Where( e => e.IsEquipment() ).ToArray();
|
||||
public static readonly EquipSlot[] AccessorySlots = Enum.GetValues< EquipSlot >().Where( e => e.IsAccessory() ).ToArray();
|
||||
public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat( AccessorySlots ).ToArray();
|
||||
}
|
||||
|
||||
public static partial class Names
|
||||
{
|
||||
public static readonly Dictionary< string, EquipSlot > SuffixToEquipSlot = new()
|
||||
{
|
||||
{ EquipSlot.Head.ToSuffix(), EquipSlot.Head },
|
||||
{ EquipSlot.Hands.ToSuffix(), EquipSlot.Hands },
|
||||
{ EquipSlot.Legs.ToSuffix(), EquipSlot.Legs },
|
||||
{ EquipSlot.Feet.ToSuffix(), EquipSlot.Feet },
|
||||
{ EquipSlot.Body.ToSuffix(), EquipSlot.Body },
|
||||
{ EquipSlot.Ears.ToSuffix(), EquipSlot.Ears },
|
||||
{ EquipSlot.Neck.ToSuffix(), EquipSlot.Neck },
|
||||
{ EquipSlot.RFinger.ToSuffix(), EquipSlot.RFinger },
|
||||
{ EquipSlot.LFinger.ToSuffix(), EquipSlot.LFinger },
|
||||
{ EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists },
|
||||
};
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Penumbra.GameData.Enums
|
||||
{
|
||||
public enum FileType : byte
|
||||
{
|
||||
Unknown,
|
||||
Sound,
|
||||
Imc,
|
||||
Vfx,
|
||||
Animation,
|
||||
Pap,
|
||||
MetaInfo,
|
||||
Material,
|
||||
Texture,
|
||||
Model,
|
||||
Shader,
|
||||
Font,
|
||||
Environment,
|
||||
}
|
||||
|
||||
public static partial class Names
|
||||
{
|
||||
public static readonly Dictionary< string, FileType > ExtensionToFileType = new()
|
||||
{
|
||||
{ ".mdl", FileType.Model },
|
||||
{ ".tex", FileType.Texture },
|
||||
{ ".mtrl", FileType.Material },
|
||||
{ ".atex", FileType.Animation },
|
||||
{ ".avfx", FileType.Vfx },
|
||||
{ ".scd", FileType.Sound },
|
||||
{ ".imc", FileType.Imc },
|
||||
{ ".pap", FileType.Pap },
|
||||
{ ".eqp", FileType.MetaInfo },
|
||||
{ ".eqdp", FileType.MetaInfo },
|
||||
{ ".est", FileType.MetaInfo },
|
||||
{ ".exd", FileType.MetaInfo },
|
||||
{ ".exh", FileType.MetaInfo },
|
||||
{ ".shpk", FileType.Shader },
|
||||
{ ".shcd", FileType.Shader },
|
||||
{ ".fdt", FileType.Font },
|
||||
{ ".envb", FileType.Environment },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace Penumbra.GameData.Enums
|
||||
{
|
||||
public enum MouseButton
|
||||
{
|
||||
None,
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace Penumbra.GameData.Enums;
|
||||
|
||||
public enum ObjectType : byte
|
||||
{
|
||||
Unknown,
|
||||
Vfx,
|
||||
DemiHuman,
|
||||
Accessory,
|
||||
World,
|
||||
Housing,
|
||||
Monster,
|
||||
Icon,
|
||||
LoadingScreen,
|
||||
Map,
|
||||
Interface,
|
||||
Equipment,
|
||||
Character,
|
||||
Weapon,
|
||||
Font,
|
||||
}
|
||||
|
||||
public static class ObjectTypeExtensions
|
||||
{
|
||||
public static string ToName( this ObjectType type )
|
||||
=> type switch
|
||||
{
|
||||
ObjectType.Vfx => "Visual Effect",
|
||||
ObjectType.DemiHuman => "Demi Human",
|
||||
ObjectType.Accessory => "Accessory",
|
||||
ObjectType.World => "Doodad",
|
||||
ObjectType.Housing => "Housing Object",
|
||||
ObjectType.Monster => "Monster",
|
||||
ObjectType.Icon => "Icon",
|
||||
ObjectType.LoadingScreen => "Loading Screen",
|
||||
ObjectType.Map => "Map",
|
||||
ObjectType.Interface => "UI Element",
|
||||
ObjectType.Equipment => "Equipment",
|
||||
ObjectType.Character => "Character",
|
||||
ObjectType.Weapon => "Weapon",
|
||||
ObjectType.Font => "Font",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
|
||||
public static readonly ObjectType[] ValidImcTypes =
|
||||
{
|
||||
ObjectType.Equipment,
|
||||
ObjectType.Accessory,
|
||||
ObjectType.DemiHuman,
|
||||
ObjectType.Monster,
|
||||
ObjectType.Weapon,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,448 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Penumbra.GameData.Enums;
|
||||
|
||||
public enum Race : byte
|
||||
{
|
||||
Unknown,
|
||||
Hyur,
|
||||
Elezen,
|
||||
Lalafell,
|
||||
Miqote,
|
||||
Roegadyn,
|
||||
AuRa,
|
||||
Hrothgar,
|
||||
Viera,
|
||||
}
|
||||
|
||||
public enum Gender : byte
|
||||
{
|
||||
Unknown,
|
||||
Male,
|
||||
Female,
|
||||
MaleNpc,
|
||||
FemaleNpc,
|
||||
}
|
||||
|
||||
public enum ModelRace : byte
|
||||
{
|
||||
Unknown,
|
||||
Midlander,
|
||||
Highlander,
|
||||
Elezen,
|
||||
Lalafell,
|
||||
Miqote,
|
||||
Roegadyn,
|
||||
AuRa,
|
||||
Hrothgar,
|
||||
Viera,
|
||||
}
|
||||
|
||||
public enum SubRace : byte
|
||||
{
|
||||
Unknown,
|
||||
Midlander,
|
||||
Highlander,
|
||||
Wildwood,
|
||||
Duskwight,
|
||||
Plainsfolk,
|
||||
Dunesfolk,
|
||||
SeekerOfTheSun,
|
||||
KeeperOfTheMoon,
|
||||
Seawolf,
|
||||
Hellsguard,
|
||||
Raen,
|
||||
Xaela,
|
||||
Helion,
|
||||
Lost,
|
||||
Rava,
|
||||
Veena,
|
||||
}
|
||||
|
||||
// The combined gender-race-npc numerical code as used by the game.
|
||||
public enum GenderRace : ushort
|
||||
{
|
||||
Unknown = 0,
|
||||
MidlanderMale = 0101,
|
||||
MidlanderMaleNpc = 0104,
|
||||
MidlanderFemale = 0201,
|
||||
MidlanderFemaleNpc = 0204,
|
||||
HighlanderMale = 0301,
|
||||
HighlanderMaleNpc = 0304,
|
||||
HighlanderFemale = 0401,
|
||||
HighlanderFemaleNpc = 0404,
|
||||
ElezenMale = 0501,
|
||||
ElezenMaleNpc = 0504,
|
||||
ElezenFemale = 0601,
|
||||
ElezenFemaleNpc = 0604,
|
||||
MiqoteMale = 0701,
|
||||
MiqoteMaleNpc = 0704,
|
||||
MiqoteFemale = 0801,
|
||||
MiqoteFemaleNpc = 0804,
|
||||
RoegadynMale = 0901,
|
||||
RoegadynMaleNpc = 0904,
|
||||
RoegadynFemale = 1001,
|
||||
RoegadynFemaleNpc = 1004,
|
||||
LalafellMale = 1101,
|
||||
LalafellMaleNpc = 1104,
|
||||
LalafellFemale = 1201,
|
||||
LalafellFemaleNpc = 1204,
|
||||
AuRaMale = 1301,
|
||||
AuRaMaleNpc = 1304,
|
||||
AuRaFemale = 1401,
|
||||
AuRaFemaleNpc = 1404,
|
||||
HrothgarMale = 1501,
|
||||
HrothgarMaleNpc = 1504,
|
||||
HrothgarFemale = 1601,
|
||||
HrothgarFemaleNpc = 1604,
|
||||
VieraMale = 1701,
|
||||
VieraMaleNpc = 1704,
|
||||
VieraFemale = 1801,
|
||||
VieraFemaleNpc = 1804,
|
||||
UnknownMaleNpc = 9104,
|
||||
UnknownFemaleNpc = 9204,
|
||||
}
|
||||
|
||||
public static class RaceEnumExtensions
|
||||
{
|
||||
public static Race ToRace( this ModelRace race )
|
||||
{
|
||||
return race switch
|
||||
{
|
||||
ModelRace.Unknown => Race.Unknown,
|
||||
ModelRace.Midlander => Race.Hyur,
|
||||
ModelRace.Highlander => Race.Hyur,
|
||||
ModelRace.Elezen => Race.Elezen,
|
||||
ModelRace.Lalafell => Race.Lalafell,
|
||||
ModelRace.Miqote => Race.Miqote,
|
||||
ModelRace.Roegadyn => Race.Roegadyn,
|
||||
ModelRace.AuRa => Race.AuRa,
|
||||
ModelRace.Hrothgar => Race.Hrothgar,
|
||||
ModelRace.Viera => Race.Viera,
|
||||
_ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ),
|
||||
};
|
||||
}
|
||||
|
||||
public static Race ToRace( this SubRace subRace )
|
||||
{
|
||||
return subRace switch
|
||||
{
|
||||
SubRace.Unknown => Race.Unknown,
|
||||
SubRace.Midlander => Race.Hyur,
|
||||
SubRace.Highlander => Race.Hyur,
|
||||
SubRace.Wildwood => Race.Elezen,
|
||||
SubRace.Duskwight => Race.Elezen,
|
||||
SubRace.Plainsfolk => Race.Lalafell,
|
||||
SubRace.Dunesfolk => Race.Lalafell,
|
||||
SubRace.SeekerOfTheSun => Race.Miqote,
|
||||
SubRace.KeeperOfTheMoon => Race.Miqote,
|
||||
SubRace.Seawolf => Race.Roegadyn,
|
||||
SubRace.Hellsguard => Race.Roegadyn,
|
||||
SubRace.Raen => Race.AuRa,
|
||||
SubRace.Xaela => Race.AuRa,
|
||||
SubRace.Helion => Race.Hrothgar,
|
||||
SubRace.Lost => Race.Hrothgar,
|
||||
SubRace.Rava => Race.Viera,
|
||||
SubRace.Veena => Race.Viera,
|
||||
_ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ),
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToName( this ModelRace modelRace )
|
||||
{
|
||||
return modelRace switch
|
||||
{
|
||||
ModelRace.Midlander => SubRace.Midlander.ToName(),
|
||||
ModelRace.Highlander => SubRace.Highlander.ToName(),
|
||||
ModelRace.Elezen => Race.Elezen.ToName(),
|
||||
ModelRace.Lalafell => Race.Lalafell.ToName(),
|
||||
ModelRace.Miqote => Race.Miqote.ToName(),
|
||||
ModelRace.Roegadyn => Race.Roegadyn.ToName(),
|
||||
ModelRace.AuRa => Race.AuRa.ToName(),
|
||||
ModelRace.Hrothgar => Race.Hrothgar.ToName(),
|
||||
ModelRace.Viera => Race.Viera.ToName(),
|
||||
_ => Race.Unknown.ToName(),
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToName( this Race race )
|
||||
{
|
||||
return race switch
|
||||
{
|
||||
Race.Hyur => "Hyur",
|
||||
Race.Elezen => "Elezen",
|
||||
Race.Lalafell => "Lalafell",
|
||||
Race.Miqote => "Miqo'te",
|
||||
Race.Roegadyn => "Roegadyn",
|
||||
Race.AuRa => "Au Ra",
|
||||
Race.Hrothgar => "Hrothgar",
|
||||
Race.Viera => "Viera",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToName( this Gender gender )
|
||||
{
|
||||
return gender switch
|
||||
{
|
||||
Gender.Male => "Male",
|
||||
Gender.Female => "Female",
|
||||
Gender.MaleNpc => "Male (NPC)",
|
||||
Gender.FemaleNpc => "Female (NPC)",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToName( this SubRace subRace )
|
||||
{
|
||||
return subRace switch
|
||||
{
|
||||
SubRace.Midlander => "Midlander",
|
||||
SubRace.Highlander => "Highlander",
|
||||
SubRace.Wildwood => "Wildwood",
|
||||
SubRace.Duskwight => "Duskwright",
|
||||
SubRace.Plainsfolk => "Plainsfolk",
|
||||
SubRace.Dunesfolk => "Dunesfolk",
|
||||
SubRace.SeekerOfTheSun => "Seeker Of The Sun",
|
||||
SubRace.KeeperOfTheMoon => "Keeper Of The Moon",
|
||||
SubRace.Seawolf => "Seawolf",
|
||||
SubRace.Hellsguard => "Hellsguard",
|
||||
SubRace.Raen => "Raen",
|
||||
SubRace.Xaela => "Xaela",
|
||||
SubRace.Helion => "Hellion",
|
||||
SubRace.Lost => "Lost",
|
||||
SubRace.Rava => "Rava",
|
||||
SubRace.Veena => "Veena",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
public static bool FitsRace( this SubRace subRace, Race race )
|
||||
=> subRace.ToRace() == race;
|
||||
|
||||
public static byte ToByte( this Gender gender, ModelRace modelRace )
|
||||
=> ( byte )( ( int )gender | ( ( int )modelRace << 3 ) );
|
||||
|
||||
public static byte ToByte( this ModelRace modelRace, Gender gender )
|
||||
=> gender.ToByte( modelRace );
|
||||
|
||||
public static byte ToByte( this GenderRace value )
|
||||
{
|
||||
var (gender, race) = value.Split();
|
||||
return gender.ToByte( race );
|
||||
}
|
||||
|
||||
public static (Gender, ModelRace) Split( this GenderRace value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
GenderRace.Unknown => ( Gender.Unknown, ModelRace.Unknown ),
|
||||
GenderRace.MidlanderMale => ( Gender.Male, ModelRace.Midlander ),
|
||||
GenderRace.MidlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Midlander ),
|
||||
GenderRace.MidlanderFemale => ( Gender.Female, ModelRace.Midlander ),
|
||||
GenderRace.MidlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Midlander ),
|
||||
GenderRace.HighlanderMale => ( Gender.Male, ModelRace.Highlander ),
|
||||
GenderRace.HighlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Highlander ),
|
||||
GenderRace.HighlanderFemale => ( Gender.Female, ModelRace.Highlander ),
|
||||
GenderRace.HighlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Highlander ),
|
||||
GenderRace.ElezenMale => ( Gender.Male, ModelRace.Elezen ),
|
||||
GenderRace.ElezenMaleNpc => ( Gender.MaleNpc, ModelRace.Elezen ),
|
||||
GenderRace.ElezenFemale => ( Gender.Female, ModelRace.Elezen ),
|
||||
GenderRace.ElezenFemaleNpc => ( Gender.FemaleNpc, ModelRace.Elezen ),
|
||||
GenderRace.LalafellMale => ( Gender.Male, ModelRace.Lalafell ),
|
||||
GenderRace.LalafellMaleNpc => ( Gender.MaleNpc, ModelRace.Lalafell ),
|
||||
GenderRace.LalafellFemale => ( Gender.Female, ModelRace.Lalafell ),
|
||||
GenderRace.LalafellFemaleNpc => ( Gender.FemaleNpc, ModelRace.Lalafell ),
|
||||
GenderRace.MiqoteMale => ( Gender.Male, ModelRace.Miqote ),
|
||||
GenderRace.MiqoteMaleNpc => ( Gender.MaleNpc, ModelRace.Miqote ),
|
||||
GenderRace.MiqoteFemale => ( Gender.Female, ModelRace.Miqote ),
|
||||
GenderRace.MiqoteFemaleNpc => ( Gender.FemaleNpc, ModelRace.Miqote ),
|
||||
GenderRace.RoegadynMale => ( Gender.Male, ModelRace.Roegadyn ),
|
||||
GenderRace.RoegadynMaleNpc => ( Gender.MaleNpc, ModelRace.Roegadyn ),
|
||||
GenderRace.RoegadynFemale => ( Gender.Female, ModelRace.Roegadyn ),
|
||||
GenderRace.RoegadynFemaleNpc => ( Gender.FemaleNpc, ModelRace.Roegadyn ),
|
||||
GenderRace.AuRaMale => ( Gender.Male, ModelRace.AuRa ),
|
||||
GenderRace.AuRaMaleNpc => ( Gender.MaleNpc, ModelRace.AuRa ),
|
||||
GenderRace.AuRaFemale => ( Gender.Female, ModelRace.AuRa ),
|
||||
GenderRace.AuRaFemaleNpc => ( Gender.FemaleNpc, ModelRace.AuRa ),
|
||||
GenderRace.HrothgarMale => ( Gender.Male, ModelRace.Hrothgar ),
|
||||
GenderRace.HrothgarMaleNpc => ( Gender.MaleNpc, ModelRace.Hrothgar ),
|
||||
GenderRace.HrothgarFemale => ( Gender.Female, ModelRace.Hrothgar ),
|
||||
GenderRace.HrothgarFemaleNpc => ( Gender.FemaleNpc, ModelRace.Hrothgar ),
|
||||
GenderRace.VieraMale => ( Gender.Male, ModelRace.Viera ),
|
||||
GenderRace.VieraMaleNpc => ( Gender.Male, ModelRace.Viera ),
|
||||
GenderRace.VieraFemale => ( Gender.Female, ModelRace.Viera ),
|
||||
GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, ModelRace.Viera ),
|
||||
GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, ModelRace.Unknown ),
|
||||
GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, ModelRace.Unknown ),
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsValid( this GenderRace value )
|
||||
=> value != GenderRace.Unknown && Enum.IsDefined( typeof( GenderRace ), value );
|
||||
|
||||
public static string ToRaceCode( this GenderRace value )
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
GenderRace.MidlanderMale => "0101",
|
||||
GenderRace.MidlanderMaleNpc => "0104",
|
||||
GenderRace.MidlanderFemale => "0201",
|
||||
GenderRace.MidlanderFemaleNpc => "0204",
|
||||
GenderRace.HighlanderMale => "0301",
|
||||
GenderRace.HighlanderMaleNpc => "0304",
|
||||
GenderRace.HighlanderFemale => "0401",
|
||||
GenderRace.HighlanderFemaleNpc => "0404",
|
||||
GenderRace.ElezenMale => "0501",
|
||||
GenderRace.ElezenMaleNpc => "0504",
|
||||
GenderRace.ElezenFemale => "0601",
|
||||
GenderRace.ElezenFemaleNpc => "0604",
|
||||
GenderRace.MiqoteMale => "0701",
|
||||
GenderRace.MiqoteMaleNpc => "0704",
|
||||
GenderRace.MiqoteFemale => "0801",
|
||||
GenderRace.MiqoteFemaleNpc => "0804",
|
||||
GenderRace.RoegadynMale => "0901",
|
||||
GenderRace.RoegadynMaleNpc => "0904",
|
||||
GenderRace.RoegadynFemale => "1001",
|
||||
GenderRace.RoegadynFemaleNpc => "1004",
|
||||
GenderRace.LalafellMale => "1101",
|
||||
GenderRace.LalafellMaleNpc => "1104",
|
||||
GenderRace.LalafellFemale => "1201",
|
||||
GenderRace.LalafellFemaleNpc => "1204",
|
||||
GenderRace.AuRaMale => "1301",
|
||||
GenderRace.AuRaMaleNpc => "1304",
|
||||
GenderRace.AuRaFemale => "1401",
|
||||
GenderRace.AuRaFemaleNpc => "1404",
|
||||
GenderRace.HrothgarMale => "1501",
|
||||
GenderRace.HrothgarMaleNpc => "1504",
|
||||
GenderRace.HrothgarFemale => "1601",
|
||||
GenderRace.HrothgarFemaleNpc => "1604",
|
||||
GenderRace.VieraMale => "1701",
|
||||
GenderRace.VieraMaleNpc => "1704",
|
||||
GenderRace.VieraFemale => "1801",
|
||||
GenderRace.VieraFemaleNpc => "1804",
|
||||
GenderRace.UnknownMaleNpc => "9104",
|
||||
GenderRace.UnknownFemaleNpc => "9204",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class Names
|
||||
{
|
||||
public static GenderRace GenderRaceFromCode( string code )
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
"0101" => GenderRace.MidlanderMale,
|
||||
"0104" => GenderRace.MidlanderMaleNpc,
|
||||
"0201" => GenderRace.MidlanderFemale,
|
||||
"0204" => GenderRace.MidlanderFemaleNpc,
|
||||
"0301" => GenderRace.HighlanderMale,
|
||||
"0304" => GenderRace.HighlanderMaleNpc,
|
||||
"0401" => GenderRace.HighlanderFemale,
|
||||
"0404" => GenderRace.HighlanderFemaleNpc,
|
||||
"0501" => GenderRace.ElezenMale,
|
||||
"0504" => GenderRace.ElezenMaleNpc,
|
||||
"0601" => GenderRace.ElezenFemale,
|
||||
"0604" => GenderRace.ElezenFemaleNpc,
|
||||
"0701" => GenderRace.MiqoteMale,
|
||||
"0704" => GenderRace.MiqoteMaleNpc,
|
||||
"0801" => GenderRace.MiqoteFemale,
|
||||
"0804" => GenderRace.MiqoteFemaleNpc,
|
||||
"0901" => GenderRace.RoegadynMale,
|
||||
"0904" => GenderRace.RoegadynMaleNpc,
|
||||
"1001" => GenderRace.RoegadynFemale,
|
||||
"1004" => GenderRace.RoegadynFemaleNpc,
|
||||
"1101" => GenderRace.LalafellMale,
|
||||
"1104" => GenderRace.LalafellMaleNpc,
|
||||
"1201" => GenderRace.LalafellFemale,
|
||||
"1204" => GenderRace.LalafellFemaleNpc,
|
||||
"1301" => GenderRace.AuRaMale,
|
||||
"1304" => GenderRace.AuRaMaleNpc,
|
||||
"1401" => GenderRace.AuRaFemale,
|
||||
"1404" => GenderRace.AuRaFemaleNpc,
|
||||
"1501" => GenderRace.HrothgarMale,
|
||||
"1504" => GenderRace.HrothgarMaleNpc,
|
||||
"1601" => GenderRace.HrothgarFemale,
|
||||
"1604" => GenderRace.HrothgarFemaleNpc,
|
||||
"1701" => GenderRace.VieraMale,
|
||||
"1704" => GenderRace.VieraMaleNpc,
|
||||
"1801" => GenderRace.VieraFemale,
|
||||
"1804" => GenderRace.VieraFemaleNpc,
|
||||
"9104" => GenderRace.UnknownMaleNpc,
|
||||
"9204" => GenderRace.UnknownFemaleNpc,
|
||||
_ => throw new KeyNotFoundException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static GenderRace GenderRaceFromByte( byte value )
|
||||
{
|
||||
var gender = ( Gender )( value & 0b111 );
|
||||
var race = ( ModelRace )( value >> 3 );
|
||||
return CombinedRace( gender, race );
|
||||
}
|
||||
|
||||
public static GenderRace CombinedRace( Gender gender, ModelRace modelRace )
|
||||
{
|
||||
return gender switch
|
||||
{
|
||||
Gender.Male => modelRace switch
|
||||
{
|
||||
ModelRace.Midlander => GenderRace.MidlanderMale,
|
||||
ModelRace.Highlander => GenderRace.HighlanderMale,
|
||||
ModelRace.Elezen => GenderRace.ElezenMale,
|
||||
ModelRace.Lalafell => GenderRace.LalafellMale,
|
||||
ModelRace.Miqote => GenderRace.MiqoteMale,
|
||||
ModelRace.Roegadyn => GenderRace.RoegadynMale,
|
||||
ModelRace.AuRa => GenderRace.AuRaMale,
|
||||
ModelRace.Hrothgar => GenderRace.HrothgarMale,
|
||||
ModelRace.Viera => GenderRace.VieraMale,
|
||||
_ => GenderRace.Unknown,
|
||||
},
|
||||
Gender.MaleNpc => modelRace switch
|
||||
{
|
||||
ModelRace.Midlander => GenderRace.MidlanderMaleNpc,
|
||||
ModelRace.Highlander => GenderRace.HighlanderMaleNpc,
|
||||
ModelRace.Elezen => GenderRace.ElezenMaleNpc,
|
||||
ModelRace.Lalafell => GenderRace.LalafellMaleNpc,
|
||||
ModelRace.Miqote => GenderRace.MiqoteMaleNpc,
|
||||
ModelRace.Roegadyn => GenderRace.RoegadynMaleNpc,
|
||||
ModelRace.AuRa => GenderRace.AuRaMaleNpc,
|
||||
ModelRace.Hrothgar => GenderRace.HrothgarMaleNpc,
|
||||
ModelRace.Viera => GenderRace.VieraMaleNpc,
|
||||
_ => GenderRace.Unknown,
|
||||
},
|
||||
Gender.Female => modelRace switch
|
||||
{
|
||||
ModelRace.Midlander => GenderRace.MidlanderFemale,
|
||||
ModelRace.Highlander => GenderRace.HighlanderFemale,
|
||||
ModelRace.Elezen => GenderRace.ElezenFemale,
|
||||
ModelRace.Lalafell => GenderRace.LalafellFemale,
|
||||
ModelRace.Miqote => GenderRace.MiqoteFemale,
|
||||
ModelRace.Roegadyn => GenderRace.RoegadynFemale,
|
||||
ModelRace.AuRa => GenderRace.AuRaFemale,
|
||||
ModelRace.Hrothgar => GenderRace.HrothgarFemale,
|
||||
ModelRace.Viera => GenderRace.VieraFemale,
|
||||
_ => GenderRace.Unknown,
|
||||
},
|
||||
Gender.FemaleNpc => modelRace switch
|
||||
{
|
||||
ModelRace.Midlander => GenderRace.MidlanderFemaleNpc,
|
||||
ModelRace.Highlander => GenderRace.HighlanderFemaleNpc,
|
||||
ModelRace.Elezen => GenderRace.ElezenFemaleNpc,
|
||||
ModelRace.Lalafell => GenderRace.LalafellFemaleNpc,
|
||||
ModelRace.Miqote => GenderRace.MiqoteFemaleNpc,
|
||||
ModelRace.Roegadyn => GenderRace.RoegadynFemaleNpc,
|
||||
ModelRace.AuRa => GenderRace.AuRaFemaleNpc,
|
||||
ModelRace.Hrothgar => GenderRace.HrothgarFemaleNpc,
|
||||
ModelRace.Viera => GenderRace.VieraFemaleNpc,
|
||||
_ => GenderRace.Unknown,
|
||||
},
|
||||
_ => GenderRace.Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Penumbra.GameData.Enums;
|
||||
|
||||
public enum RedrawType
|
||||
{
|
||||
Redraw,
|
||||
AfterGPose,
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.GameData.Enums;
|
||||
|
||||
public enum ResourceType : uint
|
||||
{
|
||||
Aet = 0x00616574,
|
||||
Amb = 0x00616D62,
|
||||
Atch = 0x61746368,
|
||||
Atex = 0x61746578,
|
||||
Avfx = 0x61766678,
|
||||
Awt = 0x00617774,
|
||||
Cmp = 0x00636D70,
|
||||
Dic = 0x00646963,
|
||||
Eid = 0x00656964,
|
||||
Envb = 0x656E7662,
|
||||
Eqdp = 0x65716470,
|
||||
Eqp = 0x00657170,
|
||||
Essb = 0x65737362,
|
||||
Est = 0x00657374,
|
||||
Exd = 0x00657864,
|
||||
Exh = 0x00657868,
|
||||
Exl = 0x0065786C,
|
||||
Fdt = 0x00666474,
|
||||
Gfd = 0x00676664,
|
||||
Ggd = 0x00676764,
|
||||
Gmp = 0x00676D70,
|
||||
Gzd = 0x00677A64,
|
||||
Imc = 0x00696D63,
|
||||
Lcb = 0x006C6362,
|
||||
Lgb = 0x006C6762,
|
||||
Luab = 0x6C756162,
|
||||
Lvb = 0x006C7662,
|
||||
Mdl = 0x006D646C,
|
||||
Mlt = 0x006D6C74,
|
||||
Mtrl = 0x6D74726C,
|
||||
Obsb = 0x6F627362,
|
||||
Pap = 0x00706170,
|
||||
Pbd = 0x00706264,
|
||||
Pcb = 0x00706362,
|
||||
Phyb = 0x70687962,
|
||||
Plt = 0x00706C74,
|
||||
Scd = 0x00736364,
|
||||
Sgb = 0x00736762,
|
||||
Shcd = 0x73686364,
|
||||
Shpk = 0x7368706B,
|
||||
Sklb = 0x736B6C62,
|
||||
Skp = 0x00736B70,
|
||||
Stm = 0x0073746D,
|
||||
Svb = 0x00737662,
|
||||
Tera = 0x74657261,
|
||||
Tex = 0x00746578,
|
||||
Tmb = 0x00746D62,
|
||||
Ugd = 0x00756764,
|
||||
Uld = 0x00756C64,
|
||||
Waoe = 0x77616F65,
|
||||
Wtd = 0x00777464,
|
||||
}
|
||||
|
||||
public static class ResourceTypeExtensions
|
||||
{
|
||||
public static ResourceType FromBytes( byte a1, byte a2, byte a3 )
|
||||
=> ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 16 )
|
||||
| ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 8 )
|
||||
| ByteStringFunctions.AsciiToLower( a3 ) );
|
||||
|
||||
public static ResourceType FromBytes( byte a1, byte a2, byte a3, byte a4 )
|
||||
=> ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 24 )
|
||||
| ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 16 )
|
||||
| ( ( uint )ByteStringFunctions.AsciiToLower( a3 ) << 8 )
|
||||
| ByteStringFunctions.AsciiToLower( a4 ) );
|
||||
|
||||
public static ResourceType FromBytes( char a1, char a2, char a3 )
|
||||
=> FromBytes( ( byte )a1, ( byte )a2, ( byte )a3 );
|
||||
|
||||
public static ResourceType FromBytes( char a1, char a2, char a3, char a4 )
|
||||
=> FromBytes( ( byte )a1, ( byte )a2, ( byte )a3, ( byte )a4 );
|
||||
|
||||
public static ResourceType FromString( string path )
|
||||
{
|
||||
var ext = Path.GetExtension( path.AsSpan() );
|
||||
ext = ext.Length == 0 ? path.AsSpan() : ext[ 1.. ];
|
||||
|
||||
return ext.Length switch
|
||||
{
|
||||
0 => 0,
|
||||
1 => ( ResourceType )ext[ ^1 ],
|
||||
2 => FromBytes( '\0', ext[ ^2 ], ext[ ^1 ] ),
|
||||
3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ),
|
||||
_ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ),
|
||||
};
|
||||
}
|
||||
|
||||
public static ResourceType FromString( Utf8String path )
|
||||
{
|
||||
var extIdx = path.LastIndexOf( ( byte )'.' );
|
||||
var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? Utf8String.Empty : path.Substring( extIdx + 1 );
|
||||
|
||||
return ext.Length switch
|
||||
{
|
||||
0 => 0,
|
||||
1 => ( ResourceType )ext[ ^1 ],
|
||||
2 => FromBytes( 0, ext[ ^2 ], ext[ ^1 ] ),
|
||||
3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ),
|
||||
_ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace Penumbra.GameData.Enums
|
||||
{
|
||||
public enum RspAttribute : byte
|
||||
{
|
||||
MaleMinSize,
|
||||
MaleMaxSize,
|
||||
MaleMinTail,
|
||||
MaleMaxTail,
|
||||
FemaleMinSize,
|
||||
FemaleMaxSize,
|
||||
FemaleMinTail,
|
||||
FemaleMaxTail,
|
||||
BustMinX,
|
||||
BustMinY,
|
||||
BustMinZ,
|
||||
BustMaxX,
|
||||
BustMaxY,
|
||||
BustMaxZ,
|
||||
NumAttributes,
|
||||
}
|
||||
|
||||
public static class RspAttributeExtensions
|
||||
{
|
||||
public static Gender ToGender( this RspAttribute attribute )
|
||||
{
|
||||
return attribute switch
|
||||
{
|
||||
RspAttribute.MaleMinSize => Gender.Male,
|
||||
RspAttribute.MaleMaxSize => Gender.Male,
|
||||
RspAttribute.MaleMinTail => Gender.Male,
|
||||
RspAttribute.MaleMaxTail => Gender.Male,
|
||||
RspAttribute.FemaleMinSize => Gender.Female,
|
||||
RspAttribute.FemaleMaxSize => Gender.Female,
|
||||
RspAttribute.FemaleMinTail => Gender.Female,
|
||||
RspAttribute.FemaleMaxTail => Gender.Female,
|
||||
RspAttribute.BustMinX => Gender.Female,
|
||||
RspAttribute.BustMinY => Gender.Female,
|
||||
RspAttribute.BustMinZ => Gender.Female,
|
||||
RspAttribute.BustMaxX => Gender.Female,
|
||||
RspAttribute.BustMaxY => Gender.Female,
|
||||
RspAttribute.BustMaxZ => Gender.Female,
|
||||
_ => Gender.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToUngenderedString( this RspAttribute attribute )
|
||||
{
|
||||
return attribute switch
|
||||
{
|
||||
RspAttribute.MaleMinSize => "MinSize",
|
||||
RspAttribute.MaleMaxSize => "MaxSize",
|
||||
RspAttribute.MaleMinTail => "MinTail",
|
||||
RspAttribute.MaleMaxTail => "MaxTail",
|
||||
RspAttribute.FemaleMinSize => "MinSize",
|
||||
RspAttribute.FemaleMaxSize => "MaxSize",
|
||||
RspAttribute.FemaleMinTail => "MinTail",
|
||||
RspAttribute.FemaleMaxTail => "MaxTail",
|
||||
RspAttribute.BustMinX => "BustMinX",
|
||||
RspAttribute.BustMinY => "BustMinY",
|
||||
RspAttribute.BustMinZ => "BustMinZ",
|
||||
RspAttribute.BustMaxX => "BustMaxX",
|
||||
RspAttribute.BustMaxY => "BustMaxY",
|
||||
RspAttribute.BustMaxZ => "BustMaxZ",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToFullString( this RspAttribute attribute )
|
||||
{
|
||||
return attribute switch
|
||||
{
|
||||
RspAttribute.MaleMinSize => "Male Minimum Size",
|
||||
RspAttribute.MaleMaxSize => "Male Maximum Size",
|
||||
RspAttribute.FemaleMinSize => "Female Minimum Size",
|
||||
RspAttribute.FemaleMaxSize => "Female Maximum Size",
|
||||
RspAttribute.BustMinX => "Bust Minimum X-Axis",
|
||||
RspAttribute.BustMaxX => "Bust Maximum X-Axis",
|
||||
RspAttribute.BustMinY => "Bust Minimum Y-Axis",
|
||||
RspAttribute.BustMaxY => "Bust Maximum Y-Axis",
|
||||
RspAttribute.BustMinZ => "Bust Minimum Z-Axis",
|
||||
RspAttribute.BustMaxZ => "Bust Maximum Z-Axis",
|
||||
RspAttribute.MaleMinTail => "Male Minimum Tail Length",
|
||||
RspAttribute.MaleMaxTail => "Male Maximum Tail Length",
|
||||
RspAttribute.FemaleMinTail => "Female Minimum Tail Length",
|
||||
RspAttribute.FemaleMaxTail => "Female Maximum Tail Length",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,317 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Lumina.Data.Parsing;
|
||||
|
||||
namespace Penumbra.GameData.Files;
|
||||
|
||||
public partial class MdlFile
|
||||
{
|
||||
private static uint Write( BinaryWriter w, string s, long basePos )
|
||||
{
|
||||
var currentPos = w.BaseStream.Position;
|
||||
w.Write( Encoding.UTF8.GetBytes( s ) );
|
||||
w.Write( ( byte )0 );
|
||||
return ( uint )( currentPos - basePos );
|
||||
}
|
||||
|
||||
private List< uint > WriteStrings( BinaryWriter w )
|
||||
{
|
||||
var startPos = ( int )w.BaseStream.Position;
|
||||
var basePos = startPos + 8;
|
||||
var count = ( ushort )( Attributes.Length + Bones.Length + Materials.Length + Shapes.Length );
|
||||
|
||||
w.Write( count );
|
||||
w.Seek( basePos, SeekOrigin.Begin );
|
||||
var ret = Attributes.Concat( Bones )
|
||||
.Concat( Materials )
|
||||
.Concat( Shapes.Select( s => s.ShapeName ) )
|
||||
.Select( attribute => Write( w, attribute, basePos ) ).ToList();
|
||||
|
||||
w.Write( ( ushort )0 ); // Seems to always have two additional null-bytes, not padding.
|
||||
var size = ( int )w.BaseStream.Position - basePos;
|
||||
w.Seek( startPos + 4, SeekOrigin.Begin );
|
||||
w.Write( ( uint )size );
|
||||
w.Seek( basePos + size, SeekOrigin.Begin );
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void WriteModelFileHeader( BinaryWriter w, uint runtimeSize )
|
||||
{
|
||||
w.Write( Version );
|
||||
w.Write( StackSize );
|
||||
w.Write( runtimeSize );
|
||||
w.Write( ( ushort )VertexDeclarations.Length );
|
||||
w.Write( ( ushort )Materials.Length );
|
||||
w.Write( VertexOffset[ 0 ] > 0 ? VertexOffset[ 0 ] + runtimeSize : 0u );
|
||||
w.Write( VertexOffset[ 1 ] > 0 ? VertexOffset[ 1 ] + runtimeSize : 0u );
|
||||
w.Write( VertexOffset[ 2 ] > 0 ? VertexOffset[ 2 ] + runtimeSize : 0u );
|
||||
w.Write( IndexOffset[ 0 ] > 0 ? IndexOffset[ 0 ] + runtimeSize : 0u );
|
||||
w.Write( IndexOffset[ 1 ] > 0 ? IndexOffset[ 1 ] + runtimeSize : 0u );
|
||||
w.Write( IndexOffset[ 2 ] > 0 ? IndexOffset[ 2 ] + runtimeSize : 0u );
|
||||
w.Write( VertexBufferSize[ 0 ] );
|
||||
w.Write( VertexBufferSize[ 1 ] );
|
||||
w.Write( VertexBufferSize[ 2 ] );
|
||||
w.Write( IndexBufferSize[ 0 ] );
|
||||
w.Write( IndexBufferSize[ 1 ] );
|
||||
w.Write( IndexBufferSize[ 2 ] );
|
||||
w.Write( LodCount );
|
||||
w.Write( EnableIndexBufferStreaming );
|
||||
w.Write( EnableEdgeGeometry );
|
||||
w.Write( ( byte )0 ); // Padding
|
||||
}
|
||||
|
||||
private void WriteModelHeader( BinaryWriter w )
|
||||
{
|
||||
w.Write( Radius );
|
||||
w.Write( ( ushort )Meshes.Length );
|
||||
w.Write( ( ushort )Attributes.Length );
|
||||
w.Write( ( ushort )SubMeshes.Length );
|
||||
w.Write( ( ushort )Materials.Length );
|
||||
w.Write( ( ushort )Bones.Length );
|
||||
w.Write( ( ushort )BoneTables.Length );
|
||||
w.Write( ( ushort )Shapes.Length );
|
||||
w.Write( ( ushort )ShapeMeshes.Length );
|
||||
w.Write( ( ushort )ShapeValues.Length );
|
||||
w.Write( LodCount );
|
||||
w.Write( ( byte )Flags1 );
|
||||
w.Write( ( ushort )ElementIds.Length );
|
||||
w.Write( ( byte )TerrainShadowMeshes.Length );
|
||||
w.Write( ( byte )Flags2 );
|
||||
w.Write( ModelClipOutDistance );
|
||||
w.Write( ShadowClipOutDistance );
|
||||
w.Write( Unknown4 );
|
||||
w.Write( ( ushort )TerrainShadowSubMeshes.Length );
|
||||
w.Write( Unknown5 );
|
||||
w.Write( BgChangeMaterialIndex );
|
||||
w.Write( BgCrestChangeMaterialIndex );
|
||||
w.Write( Unknown6 );
|
||||
w.Write( Unknown7 );
|
||||
w.Write( Unknown8 );
|
||||
w.Write( Unknown9 );
|
||||
w.Write( ( uint )0 ); // 6 byte padding
|
||||
w.Write( ( ushort )0 );
|
||||
}
|
||||
|
||||
|
||||
private static void Write( BinaryWriter w, in MdlStructs.VertexElement vertex )
|
||||
{
|
||||
w.Write( vertex.Stream );
|
||||
w.Write( vertex.Offset );
|
||||
w.Write( vertex.Type );
|
||||
w.Write( vertex.Usage );
|
||||
w.Write( vertex.UsageIndex );
|
||||
w.Write( ( ushort )0 ); // 3 byte padding
|
||||
w.Write( ( byte )0 );
|
||||
}
|
||||
|
||||
private static void Write( BinaryWriter w, in MdlStructs.VertexDeclarationStruct vertexDecl )
|
||||
{
|
||||
foreach( var vertex in vertexDecl.VertexElements )
|
||||
{
|
||||
Write( w, vertex );
|
||||
}
|
||||
|
||||
Write( w, new MdlStructs.VertexElement() { Stream = 255 } );
|
||||
w.Seek( ( int )( NumVertices - 1 - vertexDecl.VertexElements.Length ) * 8, SeekOrigin.Current );
|
||||
}
|
||||
|
||||
private static void Write( BinaryWriter w, in MdlStructs.ElementIdStruct elementId )
|
||||
{
|
||||
w.Write( elementId.ElementId );
|
||||
w.Write( elementId.ParentBoneName );
|
||||
w.Write( elementId.Translate[ 0 ] );
|
||||
w.Write( elementId.Translate[ 1 ] );
|
||||
w.Write( elementId.Translate[ 2 ] );
|
||||
w.Write( elementId.Rotate[ 0 ] );
|
||||
w.Write( elementId.Rotate[ 1 ] );
|
||||
w.Write( elementId.Rotate[ 2 ] );
|
||||
}
|
||||
|
||||
private static unsafe void Write< T >( BinaryWriter w, in T data ) where T : unmanaged
|
||||
{
|
||||
fixed( T* ptr = &data )
|
||||
{
|
||||
var bytePtr = ( byte* )ptr;
|
||||
var size = sizeof( T );
|
||||
var span = new ReadOnlySpan< byte >( bytePtr, size );
|
||||
w.Write( span );
|
||||
}
|
||||
}
|
||||
|
||||
private static void Write( BinaryWriter w, MdlStructs.MeshStruct mesh )
|
||||
{
|
||||
w.Write( mesh.VertexCount );
|
||||
w.Write( ( ushort )0 ); // padding
|
||||
w.Write( mesh.IndexCount );
|
||||
w.Write( mesh.MaterialIndex );
|
||||
w.Write( mesh.SubMeshIndex );
|
||||
w.Write( mesh.SubMeshCount );
|
||||
w.Write( mesh.BoneTableIndex );
|
||||
w.Write( mesh.StartIndex );
|
||||
w.Write( mesh.VertexBufferOffset[ 0 ] );
|
||||
w.Write( mesh.VertexBufferOffset[ 1 ] );
|
||||
w.Write( mesh.VertexBufferOffset[ 2 ] );
|
||||
w.Write( mesh.VertexBufferStride[ 0 ] );
|
||||
w.Write( mesh.VertexBufferStride[ 1 ] );
|
||||
w.Write( mesh.VertexBufferStride[ 2 ] );
|
||||
w.Write( mesh.VertexStreamCount );
|
||||
}
|
||||
|
||||
private static void Write( BinaryWriter w, MdlStructs.BoneTableStruct bone )
|
||||
{
|
||||
foreach( var index in bone.BoneIndex )
|
||||
{
|
||||
w.Write( index );
|
||||
}
|
||||
|
||||
w.Write( bone.BoneCount );
|
||||
w.Write( ( ushort )0 ); // 3 bytes padding
|
||||
w.Write( ( byte )0 );
|
||||
}
|
||||
|
||||
private void Write( BinaryWriter w, int shapeIdx, IReadOnlyList< uint > offsets )
|
||||
{
|
||||
var shape = Shapes[ shapeIdx ];
|
||||
var offset = offsets[ Attributes.Length + Bones.Length + Materials.Length + shapeIdx ];
|
||||
w.Write( offset );
|
||||
w.Write( shape.ShapeMeshStartIndex[ 0 ] );
|
||||
w.Write( shape.ShapeMeshStartIndex[ 1 ] );
|
||||
w.Write( shape.ShapeMeshStartIndex[ 2 ] );
|
||||
w.Write( shape.ShapeMeshCount[ 0 ] );
|
||||
w.Write( shape.ShapeMeshCount[ 1 ] );
|
||||
w.Write( shape.ShapeMeshCount[ 2 ] );
|
||||
}
|
||||
|
||||
private static void Write( BinaryWriter w, MdlStructs.BoundingBoxStruct box )
|
||||
{
|
||||
w.Write( box.Min[ 0 ] );
|
||||
w.Write( box.Min[ 1 ] );
|
||||
w.Write( box.Min[ 2 ] );
|
||||
w.Write( box.Min[ 3 ] );
|
||||
w.Write( box.Max[ 0 ] );
|
||||
w.Write( box.Max[ 1 ] );
|
||||
w.Write( box.Max[ 2 ] );
|
||||
w.Write( box.Max[ 3 ] );
|
||||
}
|
||||
|
||||
public byte[] Write()
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using( var w = new BinaryWriter( stream ) )
|
||||
{
|
||||
// Skip and write this later when we actually know it.
|
||||
w.Seek( ( int )FileHeaderSize, SeekOrigin.Begin );
|
||||
|
||||
foreach( var vertexDecl in VertexDeclarations )
|
||||
{
|
||||
Write( w, vertexDecl );
|
||||
}
|
||||
|
||||
var offsets = WriteStrings( w );
|
||||
WriteModelHeader( w );
|
||||
|
||||
foreach( var elementId in ElementIds )
|
||||
{
|
||||
Write( w, elementId );
|
||||
}
|
||||
|
||||
foreach( var lod in Lods )
|
||||
{
|
||||
Write( w, lod );
|
||||
}
|
||||
|
||||
if( Flags2.HasFlag( MdlStructs.ModelFlags2.ExtraLodEnabled ) )
|
||||
{
|
||||
foreach( var extraLod in ExtraLods )
|
||||
{
|
||||
Write( w, extraLod );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var mesh in Meshes )
|
||||
{
|
||||
Write( w, mesh );
|
||||
}
|
||||
|
||||
for( var i = 0; i < Attributes.Length; ++i )
|
||||
{
|
||||
w.Write( offsets[ i ] );
|
||||
}
|
||||
|
||||
foreach( var terrainShadowMesh in TerrainShadowMeshes )
|
||||
{
|
||||
Write( w, terrainShadowMesh );
|
||||
}
|
||||
|
||||
foreach( var subMesh in SubMeshes )
|
||||
{
|
||||
Write( w, subMesh );
|
||||
}
|
||||
|
||||
foreach( var terrainShadowSubMesh in TerrainShadowSubMeshes )
|
||||
{
|
||||
Write( w, terrainShadowSubMesh );
|
||||
}
|
||||
|
||||
for( var i = 0; i < Materials.Length; ++i )
|
||||
{
|
||||
w.Write( offsets[ Attributes.Length + Bones.Length + i ] );
|
||||
}
|
||||
|
||||
for( var i = 0; i < Bones.Length; ++i )
|
||||
{
|
||||
w.Write( offsets[ Attributes.Length + i ] );
|
||||
}
|
||||
|
||||
foreach( var boneTable in BoneTables )
|
||||
{
|
||||
Write( w, boneTable );
|
||||
}
|
||||
|
||||
for( var i = 0; i < Shapes.Length; ++i )
|
||||
{
|
||||
Write( w, i, offsets );
|
||||
}
|
||||
|
||||
foreach( var shapeMesh in ShapeMeshes )
|
||||
{
|
||||
Write( w, shapeMesh );
|
||||
}
|
||||
|
||||
foreach( var shapeValue in ShapeValues )
|
||||
{
|
||||
Write( w, shapeValue );
|
||||
}
|
||||
|
||||
w.Write( SubMeshBoneMap.Length * 2 );
|
||||
foreach( var bone in SubMeshBoneMap )
|
||||
{
|
||||
w.Write( bone );
|
||||
}
|
||||
|
||||
w.Write( ( byte )0 ); // number of padding bytes, which is 0 for us.
|
||||
|
||||
Write( w, BoundingBoxes );
|
||||
Write( w, ModelBoundingBoxes );
|
||||
Write( w, WaterBoundingBoxes );
|
||||
Write( w, VerticalFogBoundingBoxes );
|
||||
foreach( var box in BoneBoundingBoxes )
|
||||
{
|
||||
Write( w, box );
|
||||
}
|
||||
|
||||
var totalSize = w.BaseStream.Position;
|
||||
var runtimeSize = ( uint )( totalSize - StackSize - FileHeaderSize );
|
||||
w.Write( RemainingData );
|
||||
|
||||
// Write header data.
|
||||
w.Seek( 0, SeekOrigin.Begin );
|
||||
WriteModelFileHeader( w, runtimeSize );
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Lumina.Data.Parsing;
|
||||
using Lumina.Extensions;
|
||||
|
||||
namespace Penumbra.GameData.Files;
|
||||
|
||||
public partial class MdlFile
|
||||
{
|
||||
public const uint NumVertices = 17;
|
||||
public const uint FileHeaderSize = 0x44;
|
||||
|
||||
// Refers to string, thus not Lumina struct.
|
||||
public struct Shape
|
||||
{
|
||||
public string ShapeName = string.Empty;
|
||||
public ushort[] ShapeMeshStartIndex;
|
||||
public ushort[] ShapeMeshCount;
|
||||
|
||||
public Shape( MdlStructs.ShapeStruct data, uint[] offsets, string[] strings )
|
||||
{
|
||||
var idx = offsets.AsSpan().IndexOf( data.StringOffset );
|
||||
ShapeName = idx >= 0 ? strings[ idx ] : string.Empty;
|
||||
ShapeMeshStartIndex = data.ShapeMeshStartIndex;
|
||||
ShapeMeshCount = data.ShapeMeshCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Raw data to write back.
|
||||
public uint Version;
|
||||
public float Radius;
|
||||
public float ModelClipOutDistance;
|
||||
public float ShadowClipOutDistance;
|
||||
public byte BgChangeMaterialIndex;
|
||||
public byte BgCrestChangeMaterialIndex;
|
||||
public ushort Unknown4;
|
||||
public byte Unknown5;
|
||||
public byte Unknown6;
|
||||
public ushort Unknown7;
|
||||
public ushort Unknown8;
|
||||
public ushort Unknown9;
|
||||
|
||||
// Offsets are stored relative to RuntimeSize instead of file start.
|
||||
public uint[] VertexOffset;
|
||||
public uint[] IndexOffset;
|
||||
|
||||
public uint[] VertexBufferSize;
|
||||
public uint[] IndexBufferSize;
|
||||
public byte LodCount;
|
||||
public bool EnableIndexBufferStreaming;
|
||||
public bool EnableEdgeGeometry;
|
||||
|
||||
|
||||
public MdlStructs.ModelFlags1 Flags1;
|
||||
public MdlStructs.ModelFlags2 Flags2;
|
||||
|
||||
public MdlStructs.BoundingBoxStruct BoundingBoxes;
|
||||
public MdlStructs.BoundingBoxStruct ModelBoundingBoxes;
|
||||
public MdlStructs.BoundingBoxStruct WaterBoundingBoxes;
|
||||
public MdlStructs.BoundingBoxStruct VerticalFogBoundingBoxes;
|
||||
|
||||
public MdlStructs.VertexDeclarationStruct[] VertexDeclarations;
|
||||
public MdlStructs.ElementIdStruct[] ElementIds;
|
||||
public MdlStructs.MeshStruct[] Meshes;
|
||||
public MdlStructs.BoneTableStruct[] BoneTables;
|
||||
public MdlStructs.BoundingBoxStruct[] BoneBoundingBoxes;
|
||||
public MdlStructs.SubmeshStruct[] SubMeshes;
|
||||
public MdlStructs.ShapeMeshStruct[] ShapeMeshes;
|
||||
public MdlStructs.ShapeValueStruct[] ShapeValues;
|
||||
public MdlStructs.TerrainShadowMeshStruct[] TerrainShadowMeshes;
|
||||
public MdlStructs.TerrainShadowSubmeshStruct[] TerrainShadowSubMeshes;
|
||||
public MdlStructs.LodStruct[] Lods;
|
||||
public MdlStructs.ExtraLodStruct[] ExtraLods;
|
||||
public ushort[] SubMeshBoneMap;
|
||||
|
||||
// Strings are written in order
|
||||
public string[] Attributes;
|
||||
public string[] Bones;
|
||||
public string[] Materials;
|
||||
public Shape[] Shapes;
|
||||
|
||||
// Raw, unparsed data.
|
||||
public byte[] RemainingData;
|
||||
|
||||
public MdlFile( byte[] data )
|
||||
{
|
||||
using var stream = new MemoryStream( data );
|
||||
using var r = new BinaryReader( stream );
|
||||
|
||||
var header = LoadModelFileHeader( r );
|
||||
LodCount = header.LodCount;
|
||||
VertexBufferSize = header.VertexBufferSize;
|
||||
IndexBufferSize = header.IndexBufferSize;
|
||||
VertexOffset = header.VertexOffset;
|
||||
IndexOffset = header.IndexOffset;
|
||||
for( var i = 0; i < 3; ++i )
|
||||
{
|
||||
if( VertexOffset[ i ] > 0 )
|
||||
{
|
||||
VertexOffset[ i ] -= header.RuntimeSize;
|
||||
}
|
||||
|
||||
if( IndexOffset[ i ] > 0 )
|
||||
{
|
||||
IndexOffset[ i ] -= header.RuntimeSize;
|
||||
}
|
||||
}
|
||||
|
||||
VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount];
|
||||
for( var i = 0; i < header.VertexDeclarationCount; ++i )
|
||||
{
|
||||
VertexDeclarations[ i ] = MdlStructs.VertexDeclarationStruct.Read( r );
|
||||
}
|
||||
|
||||
var (offsets, strings) = LoadStrings( r );
|
||||
|
||||
var modelHeader = LoadModelHeader( r );
|
||||
ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount];
|
||||
for( var i = 0; i < modelHeader.ElementIdCount; i++ )
|
||||
{
|
||||
ElementIds[ i ] = MdlStructs.ElementIdStruct.Read( r );
|
||||
}
|
||||
|
||||
Lods = r.ReadStructuresAsArray< MdlStructs.LodStruct >( 3 );
|
||||
ExtraLods = modelHeader.ExtraLodEnabled
|
||||
? r.ReadStructuresAsArray< MdlStructs.ExtraLodStruct >( 3 )
|
||||
: Array.Empty< MdlStructs.ExtraLodStruct >();
|
||||
|
||||
Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount];
|
||||
for( var i = 0; i < modelHeader.MeshCount; i++ )
|
||||
{
|
||||
Meshes[ i ] = MdlStructs.MeshStruct.Read( r );
|
||||
}
|
||||
|
||||
Attributes = new string[modelHeader.AttributeCount];
|
||||
for( var i = 0; i < modelHeader.AttributeCount; ++i )
|
||||
{
|
||||
var offset = r.ReadUInt32();
|
||||
var stringIdx = offsets.AsSpan().IndexOf( offset );
|
||||
Attributes[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty;
|
||||
}
|
||||
|
||||
TerrainShadowMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowMeshStruct >( modelHeader.TerrainShadowMeshCount );
|
||||
SubMeshes = r.ReadStructuresAsArray< MdlStructs.SubmeshStruct >( modelHeader.SubmeshCount );
|
||||
TerrainShadowSubMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowSubmeshStruct >( modelHeader.TerrainShadowSubmeshCount );
|
||||
|
||||
Materials = new string[modelHeader.MaterialCount];
|
||||
for( var i = 0; i < modelHeader.MaterialCount; ++i )
|
||||
{
|
||||
var offset = r.ReadUInt32();
|
||||
var stringIdx = offsets.AsSpan().IndexOf( offset );
|
||||
Materials[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty;
|
||||
}
|
||||
|
||||
Bones = new string[modelHeader.BoneCount];
|
||||
for( var i = 0; i < modelHeader.BoneCount; ++i )
|
||||
{
|
||||
var offset = r.ReadUInt32();
|
||||
var stringIdx = offsets.AsSpan().IndexOf( offset );
|
||||
Bones[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty;
|
||||
}
|
||||
|
||||
BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount];
|
||||
for( var i = 0; i < modelHeader.BoneTableCount; i++ )
|
||||
{
|
||||
BoneTables[ i ] = MdlStructs.BoneTableStruct.Read( r );
|
||||
}
|
||||
|
||||
Shapes = new Shape[modelHeader.ShapeCount];
|
||||
for( var i = 0; i < modelHeader.ShapeCount; i++ )
|
||||
{
|
||||
Shapes[ i ] = new Shape( MdlStructs.ShapeStruct.Read( r ), offsets, strings );
|
||||
}
|
||||
|
||||
ShapeMeshes = r.ReadStructuresAsArray< MdlStructs.ShapeMeshStruct >( modelHeader.ShapeMeshCount );
|
||||
ShapeValues = r.ReadStructuresAsArray< MdlStructs.ShapeValueStruct >( modelHeader.ShapeValueCount );
|
||||
|
||||
var submeshBoneMapSize = r.ReadUInt32();
|
||||
SubMeshBoneMap = r.ReadStructures< ushort >( ( int )submeshBoneMapSize / 2 ).ToArray();
|
||||
|
||||
var paddingAmount = r.ReadByte();
|
||||
r.Seek( r.BaseStream.Position + paddingAmount );
|
||||
|
||||
// Dunno what this first one is for?
|
||||
BoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
||||
ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
||||
WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
||||
VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
||||
BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount];
|
||||
for( var i = 0; i < modelHeader.BoneCount; i++ )
|
||||
{
|
||||
BoneBoundingBoxes[ i ] = MdlStructs.BoundingBoxStruct.Read( r );
|
||||
}
|
||||
|
||||
RemainingData = r.ReadBytes( ( int )( r.BaseStream.Length - r.BaseStream.Position ) );
|
||||
}
|
||||
|
||||
private MdlStructs.ModelFileHeader LoadModelFileHeader( BinaryReader r )
|
||||
{
|
||||
var header = MdlStructs.ModelFileHeader.Read( r );
|
||||
Version = header.Version;
|
||||
EnableIndexBufferStreaming = header.EnableIndexBufferStreaming;
|
||||
EnableEdgeGeometry = header.EnableEdgeGeometry;
|
||||
return header;
|
||||
}
|
||||
|
||||
private MdlStructs.ModelHeader LoadModelHeader( BinaryReader r )
|
||||
{
|
||||
var modelHeader = r.ReadStructure< MdlStructs.ModelHeader >();
|
||||
Radius = modelHeader.Radius;
|
||||
Flags1 = ( MdlStructs.ModelFlags1 )( modelHeader.GetType()
|
||||
.GetField( "Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader )
|
||||
?? 0 );
|
||||
Flags2 = ( MdlStructs.ModelFlags2 )( modelHeader.GetType()
|
||||
.GetField( "Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader )
|
||||
?? 0 );
|
||||
ModelClipOutDistance = modelHeader.ModelClipOutDistance;
|
||||
ShadowClipOutDistance = modelHeader.ShadowClipOutDistance;
|
||||
Unknown4 = modelHeader.Unknown4;
|
||||
Unknown5 = ( byte )( modelHeader.GetType()
|
||||
.GetField( "Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader )
|
||||
?? 0 );
|
||||
Unknown6 = modelHeader.Unknown6;
|
||||
Unknown7 = modelHeader.Unknown7;
|
||||
Unknown8 = modelHeader.Unknown8;
|
||||
Unknown9 = modelHeader.Unknown9;
|
||||
BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex;
|
||||
BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex;
|
||||
|
||||
return modelHeader;
|
||||
}
|
||||
|
||||
private static (uint[], string[]) LoadStrings( BinaryReader r )
|
||||
{
|
||||
var stringCount = r.ReadUInt16();
|
||||
r.ReadUInt16();
|
||||
var stringSize = ( int )r.ReadUInt32();
|
||||
var stringData = r.ReadBytes( stringSize );
|
||||
var start = 0;
|
||||
var strings = new string[stringCount];
|
||||
var offsets = new uint[stringCount];
|
||||
for( var i = 0; i < stringCount; ++i )
|
||||
{
|
||||
var span = stringData.AsSpan( start );
|
||||
var idx = span.IndexOf( ( byte )'\0' );
|
||||
strings[ i ] = Encoding.UTF8.GetString( span[ ..idx ] );
|
||||
offsets[ i ] = ( uint )start;
|
||||
start = start + idx + 1;
|
||||
}
|
||||
|
||||
return ( offsets, strings );
|
||||
}
|
||||
|
||||
public unsafe uint StackSize
|
||||
=> ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) );
|
||||
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud;
|
||||
using Dalamud.Data;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData;
|
||||
|
||||
public static class GameData
|
||||
{
|
||||
internal static ObjectIdentification? Identification;
|
||||
internal static readonly GamePathParser GamePathParser = new();
|
||||
|
||||
public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage )
|
||||
{
|
||||
Identification ??= new ObjectIdentification( dataManager, clientLanguage );
|
||||
return Identification;
|
||||
}
|
||||
|
||||
public static IObjectIdentifier GetIdentifier()
|
||||
{
|
||||
if( Identification == null )
|
||||
{
|
||||
throw new Exception( "Object Identification was not initialized." );
|
||||
}
|
||||
|
||||
return Identification;
|
||||
}
|
||||
|
||||
public static IGamePathParser GetGamePathParser()
|
||||
=> GamePathParser;
|
||||
}
|
||||
|
||||
public interface IObjectIdentifier
|
||||
{
|
||||
public void Identify( IDictionary< string, object? > set, GamePath path );
|
||||
public Dictionary< string, object? > Identify( GamePath path );
|
||||
public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot );
|
||||
}
|
||||
|
||||
public interface IGamePathParser
|
||||
{
|
||||
public ObjectType PathToObjectType( GamePath path );
|
||||
public GameObjectInfo GetFileInfo( GamePath path );
|
||||
public string VfxToKey( GamePath path );
|
||||
}
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData;
|
||||
|
||||
internal class GamePathParser : IGamePathParser
|
||||
{
|
||||
private const string CharacterFolder = "chara";
|
||||
private const string EquipmentFolder = "equipment";
|
||||
private const string PlayerFolder = "human";
|
||||
private const string WeaponFolder = "weapon";
|
||||
private const string AccessoryFolder = "accessory";
|
||||
private const string DemiHumanFolder = "demihuman";
|
||||
private const string MonsterFolder = "monster";
|
||||
private const string CommonFolder = "common";
|
||||
private const string UiFolder = "ui";
|
||||
private const string IconFolder = "icon";
|
||||
private const string LoadingFolder = "loadingimage";
|
||||
private const string MapFolder = "map";
|
||||
private const string InterfaceFolder = "uld";
|
||||
private const string FontFolder = "font";
|
||||
private const string HousingFolder = "hou";
|
||||
private const string VfxFolder = "vfx";
|
||||
private const string WorldFolder1 = "bgcommon";
|
||||
private const string WorldFolder2 = "bg";
|
||||
|
||||
// @formatter:off
|
||||
private readonly Dictionary<FileType, Dictionary<ObjectType, Regex[]>> _regexes = new()
|
||||
{ { FileType.Font, new Dictionary< ObjectType, Regex[] >(){ { ObjectType.Font, new Regex[]{ new(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt") } } } }
|
||||
, { FileType.Texture, new Dictionary< ObjectType, Regex[] >()
|
||||
{ { ObjectType.Icon, new Regex[]{ new(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex") } }
|
||||
, { ObjectType.Map, new Regex[]{ new(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex") } }
|
||||
, { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex") } }
|
||||
, { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex") } }
|
||||
, { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex") } }
|
||||
, { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex") } }
|
||||
, { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex") } }
|
||||
, { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture/(?'minus'(--)?)(v(?'variant'\d{2})_)?c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?(_[a-z])?_[a-z]\.tex")
|
||||
, new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")
|
||||
, new(@"chara/common/texture/skin(?'skin'.*)\.tex")
|
||||
, new(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")
|
||||
, new(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex") } } } }
|
||||
, { FileType.Model, new Dictionary< ObjectType, Regex[] >()
|
||||
{ { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl") } }
|
||||
, { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl") } }
|
||||
, { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl") } }
|
||||
, { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl") } }
|
||||
, { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl") } }
|
||||
, { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl") } } } }
|
||||
, { FileType.Material, new Dictionary< ObjectType, Regex[] >()
|
||||
{ { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]\.mtrl") } }
|
||||
, { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]\.mtrl") } }
|
||||
, { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } }
|
||||
, { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } }
|
||||
, { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } }
|
||||
, { ObjectType.Character, new Regex[]{ new( @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]\.mtrl" ) } } } }
|
||||
, { FileType.Imc, new Dictionary< ObjectType, Regex[] >()
|
||||
{ { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc") } }
|
||||
, { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } }
|
||||
, { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc") } }
|
||||
, { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc") } }
|
||||
, { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } },
|
||||
};
|
||||
// @formatter:on
|
||||
|
||||
public ObjectType PathToObjectType( GamePath path )
|
||||
{
|
||||
if( path.Empty )
|
||||
{
|
||||
return ObjectType.Unknown;
|
||||
}
|
||||
|
||||
string p = path;
|
||||
var folders = p.Split( '/' );
|
||||
if( folders.Length < 2 )
|
||||
{
|
||||
return ObjectType.Unknown;
|
||||
}
|
||||
|
||||
return folders[ 0 ] switch
|
||||
{
|
||||
CharacterFolder => folders[ 1 ] switch
|
||||
{
|
||||
EquipmentFolder => ObjectType.Equipment,
|
||||
AccessoryFolder => ObjectType.Accessory,
|
||||
WeaponFolder => ObjectType.Weapon,
|
||||
PlayerFolder => ObjectType.Character,
|
||||
DemiHumanFolder => ObjectType.DemiHuman,
|
||||
MonsterFolder => ObjectType.Monster,
|
||||
CommonFolder => ObjectType.Character,
|
||||
_ => ObjectType.Unknown,
|
||||
},
|
||||
UiFolder => folders[ 1 ] switch
|
||||
{
|
||||
IconFolder => ObjectType.Icon,
|
||||
LoadingFolder => ObjectType.LoadingScreen,
|
||||
MapFolder => ObjectType.Map,
|
||||
InterfaceFolder => ObjectType.Interface,
|
||||
_ => ObjectType.Unknown,
|
||||
},
|
||||
CommonFolder => folders[ 1 ] switch
|
||||
{
|
||||
FontFolder => ObjectType.Font,
|
||||
_ => ObjectType.Unknown,
|
||||
},
|
||||
HousingFolder => ObjectType.Housing,
|
||||
WorldFolder1 => folders[ 1 ] switch
|
||||
{
|
||||
HousingFolder => ObjectType.Housing,
|
||||
_ => ObjectType.World,
|
||||
},
|
||||
WorldFolder2 => ObjectType.World,
|
||||
VfxFolder => ObjectType.Vfx,
|
||||
_ => ObjectType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
private (FileType, ObjectType, Match?) ParseGamePath( GamePath path )
|
||||
{
|
||||
if( !Names.ExtensionToFileType.TryGetValue( Extension( path ), out var fileType ) )
|
||||
{
|
||||
fileType = FileType.Unknown;
|
||||
}
|
||||
|
||||
var objectType = PathToObjectType( path );
|
||||
|
||||
if( !_regexes.TryGetValue( fileType, out var objectDict ) )
|
||||
{
|
||||
return ( fileType, objectType, null );
|
||||
}
|
||||
|
||||
if( !objectDict.TryGetValue( objectType, out var regexes ) )
|
||||
{
|
||||
return ( fileType, objectType, null );
|
||||
}
|
||||
|
||||
foreach( var regex in regexes )
|
||||
{
|
||||
var match = regex.Match( path );
|
||||
if( match.Success )
|
||||
{
|
||||
return ( fileType, objectType, match );
|
||||
}
|
||||
}
|
||||
|
||||
return ( fileType, objectType, null );
|
||||
}
|
||||
|
||||
private static string Extension( string filename )
|
||||
{
|
||||
var extIdx = filename.LastIndexOf( '.' );
|
||||
return extIdx < 0 ? "" : filename.Substring( extIdx );
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleEquipment( FileType fileType, GroupCollection groups )
|
||||
{
|
||||
var setId = ushort.Parse( groups[ "id" ].Value );
|
||||
if( fileType == FileType.Imc )
|
||||
{
|
||||
return GameObjectInfo.Equipment( fileType, setId );
|
||||
}
|
||||
|
||||
var gr = Names.GenderRaceFromCode( groups[ "race" ].Value );
|
||||
var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ];
|
||||
if( fileType == FileType.Model )
|
||||
{
|
||||
return GameObjectInfo.Equipment( fileType, setId, gr, slot );
|
||||
}
|
||||
|
||||
var variant = byte.Parse( groups[ "variant" ].Value );
|
||||
return GameObjectInfo.Equipment( fileType, setId, gr, slot, variant );
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleWeapon( FileType fileType, GroupCollection groups )
|
||||
{
|
||||
var weaponId = ushort.Parse( groups[ "weapon" ].Value );
|
||||
var setId = ushort.Parse( groups[ "id" ].Value );
|
||||
if( fileType == FileType.Imc || fileType == FileType.Model )
|
||||
{
|
||||
return GameObjectInfo.Weapon( fileType, setId, weaponId );
|
||||
}
|
||||
|
||||
var variant = byte.Parse( groups[ "variant" ].Value );
|
||||
return GameObjectInfo.Weapon( fileType, setId, weaponId, variant );
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleMonster( FileType fileType, GroupCollection groups )
|
||||
{
|
||||
var monsterId = ushort.Parse( groups[ "monster" ].Value );
|
||||
var bodyId = ushort.Parse( groups[ "id" ].Value );
|
||||
if( fileType == FileType.Imc || fileType == FileType.Model )
|
||||
{
|
||||
return GameObjectInfo.Monster( fileType, monsterId, bodyId );
|
||||
}
|
||||
|
||||
var variant = byte.Parse( groups[ "variant" ].Value );
|
||||
return GameObjectInfo.Monster( fileType, monsterId, bodyId, variant );
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleDemiHuman( FileType fileType, GroupCollection groups )
|
||||
{
|
||||
var demiHumanId = ushort.Parse( groups[ "id" ].Value );
|
||||
var equipId = ushort.Parse( groups[ "equip" ].Value );
|
||||
if( fileType == FileType.Imc )
|
||||
{
|
||||
return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId );
|
||||
}
|
||||
|
||||
var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ];
|
||||
if( fileType == FileType.Model )
|
||||
{
|
||||
return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot );
|
||||
}
|
||||
|
||||
var variant = byte.Parse( groups[ "variant" ].Value );
|
||||
return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot, variant );
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups )
|
||||
{
|
||||
if( groups[ "catchlight" ].Success )
|
||||
{
|
||||
return GameObjectInfo.Customization( fileType, CustomizationType.Iris );
|
||||
}
|
||||
|
||||
if( groups[ "skin" ].Success )
|
||||
{
|
||||
return GameObjectInfo.Customization( fileType, CustomizationType.Skin );
|
||||
}
|
||||
|
||||
var id = ushort.Parse( groups[ "id" ].Value );
|
||||
if( groups[ "location" ].Success )
|
||||
{
|
||||
var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace
|
||||
: groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown;
|
||||
return GameObjectInfo.Customization( fileType, tmpType, id );
|
||||
}
|
||||
|
||||
var gr = Names.GenderRaceFromCode( groups[ "race" ].Value );
|
||||
var bodySlot = Names.StringToBodySlot[ groups[ "type" ].Value ];
|
||||
var type = groups[ "slot" ].Success
|
||||
? Names.SuffixToCustomizationType[ groups[ "slot" ].Value ]
|
||||
: CustomizationType.Skin;
|
||||
if( fileType == FileType.Material )
|
||||
{
|
||||
var variant = groups[ "variant" ].Success ? byte.Parse( groups[ "variant" ].Value ) : ( byte )0;
|
||||
return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot, variant );
|
||||
}
|
||||
|
||||
return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot );
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleIcon( FileType fileType, GroupCollection groups )
|
||||
{
|
||||
var hq = groups[ "hq" ].Success;
|
||||
var hr = groups[ "hr" ].Success;
|
||||
var id = uint.Parse( groups[ "id" ].Value );
|
||||
if( !groups[ "lang" ].Success )
|
||||
{
|
||||
return GameObjectInfo.Icon( fileType, id, hq, hr );
|
||||
}
|
||||
|
||||
var language = groups[ "lang" ].Value switch
|
||||
{
|
||||
"en" => Dalamud.ClientLanguage.English,
|
||||
"ja" => Dalamud.ClientLanguage.Japanese,
|
||||
"de" => Dalamud.ClientLanguage.German,
|
||||
"fr" => Dalamud.ClientLanguage.French,
|
||||
_ => Dalamud.ClientLanguage.English,
|
||||
};
|
||||
return GameObjectInfo.Icon( fileType, id, hq, hr, language );
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleMap( FileType fileType, GroupCollection groups )
|
||||
{
|
||||
var map = Encoding.ASCII.GetBytes( groups[ "id" ].Value );
|
||||
var variant = byte.Parse( groups[ "variant" ].Value );
|
||||
if( groups[ "suffix" ].Success )
|
||||
{
|
||||
var suffix = Encoding.ASCII.GetBytes( groups[ "suffix" ].Value )[ 0 ];
|
||||
return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant, suffix );
|
||||
}
|
||||
|
||||
return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant );
|
||||
}
|
||||
|
||||
public GameObjectInfo GetFileInfo( GamePath path )
|
||||
{
|
||||
var (fileType, objectType, match) = ParseGamePath( path );
|
||||
if( match == null || !match.Success )
|
||||
{
|
||||
return new GameObjectInfo { FileType = fileType, ObjectType = objectType };
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var groups = match.Groups;
|
||||
switch( objectType )
|
||||
{
|
||||
case ObjectType.Accessory: return HandleEquipment( fileType, groups );
|
||||
case ObjectType.Equipment: return HandleEquipment( fileType, groups );
|
||||
case ObjectType.Weapon: return HandleWeapon( fileType, groups );
|
||||
case ObjectType.Map: return HandleMap( fileType, groups );
|
||||
case ObjectType.Monster: return HandleMonster( fileType, groups );
|
||||
case ObjectType.DemiHuman: return HandleDemiHuman( fileType, groups );
|
||||
case ObjectType.Character: return HandleCustomization( fileType, groups );
|
||||
case ObjectType.Icon: return HandleIcon( fileType, groups );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not parse {path}:\n{e}" );
|
||||
}
|
||||
|
||||
return new GameObjectInfo { FileType = fileType, ObjectType = objectType };
|
||||
}
|
||||
|
||||
private readonly Regex _vfxRegexTmb = new(@"chara/action/(?'key'[^\s]+?)\.tmb");
|
||||
private readonly Regex _vfxRegexPap = new(@"chara/human/c0101/animation/a0001/[^\s]+?/(?'key'[^\s]+?)\.pap");
|
||||
|
||||
public string VfxToKey( GamePath path )
|
||||
{
|
||||
var match = _vfxRegexTmb.Match( path );
|
||||
if( match.Success )
|
||||
{
|
||||
return match.Groups[ "key" ].Value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
match = _vfxRegexPap.Match( path );
|
||||
return match.Success ? match.Groups[ "key" ].Value.ToLowerInvariant() : string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,368 +0,0 @@
|
|||
using Dalamud;
|
||||
using Dalamud.Data;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.GameData.Util;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Action = Lumina.Excel.GeneratedSheets.Action;
|
||||
|
||||
namespace Penumbra.GameData;
|
||||
|
||||
internal class ObjectIdentification : IObjectIdentifier
|
||||
{
|
||||
public static DataManager? DataManager = null!;
|
||||
private readonly List< (ulong, HashSet< Item >) > _weapons;
|
||||
private readonly List< (ulong, HashSet< Item >) > _equipment;
|
||||
private readonly Dictionary< string, HashSet< Action > > _actions;
|
||||
|
||||
private static bool Add( IDictionary< ulong, HashSet< Item > > dict, ulong key, Item item )
|
||||
{
|
||||
if( dict.TryGetValue( key, out var list ) )
|
||||
{
|
||||
return list.Add( item );
|
||||
}
|
||||
|
||||
dict[ key ] = new HashSet< Item > { item };
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong EquipmentKey( Item i )
|
||||
{
|
||||
var model = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).A;
|
||||
var variant = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).B;
|
||||
var slot = ( ulong )( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot();
|
||||
return ( model << 32 ) | ( slot << 16 ) | variant;
|
||||
}
|
||||
|
||||
private static ulong WeaponKey( Item i, bool offhand )
|
||||
{
|
||||
var quad = offhand ? ( Lumina.Data.Parsing.Quad )i.ModelSub : ( Lumina.Data.Parsing.Quad )i.ModelMain;
|
||||
var model = ( ulong )quad.A;
|
||||
var type = ( ulong )quad.B;
|
||||
var variant = ( ulong )quad.C;
|
||||
|
||||
return ( model << 32 ) | ( type << 16 ) | variant;
|
||||
}
|
||||
|
||||
private void AddAction( string key, Action action )
|
||||
{
|
||||
if( key.Length == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
key = key.ToLowerInvariant();
|
||||
if( _actions.TryGetValue( key, out var actions ) )
|
||||
{
|
||||
actions.Add( action );
|
||||
}
|
||||
else
|
||||
{
|
||||
_actions[ key ] = new HashSet< Action > { action };
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectIdentification( DataManager dataManager, ClientLanguage clientLanguage )
|
||||
{
|
||||
DataManager = dataManager;
|
||||
var items = dataManager.GetExcelSheet< Item >( clientLanguage )!;
|
||||
SortedList< ulong, HashSet< Item > > weapons = new();
|
||||
SortedList< ulong, HashSet< Item > > equipment = new();
|
||||
foreach( var item in items )
|
||||
{
|
||||
switch( ( EquipSlot )item.EquipSlotCategory.Row )
|
||||
{
|
||||
case EquipSlot.MainHand:
|
||||
case EquipSlot.OffHand:
|
||||
case EquipSlot.BothHand:
|
||||
if( item.ModelMain != 0 )
|
||||
{
|
||||
Add( weapons, WeaponKey( item, false ), item );
|
||||
}
|
||||
|
||||
if( item.ModelSub != 0 )
|
||||
{
|
||||
Add( weapons, WeaponKey( item, true ), item );
|
||||
}
|
||||
|
||||
break;
|
||||
// Accessories
|
||||
case EquipSlot.RFinger:
|
||||
case EquipSlot.Wrists:
|
||||
case EquipSlot.Ears:
|
||||
case EquipSlot.Neck:
|
||||
Add( equipment, EquipmentKey( item ), item );
|
||||
break;
|
||||
// Equipment
|
||||
case EquipSlot.Head:
|
||||
case EquipSlot.Body:
|
||||
case EquipSlot.Hands:
|
||||
case EquipSlot.Legs:
|
||||
case EquipSlot.Feet:
|
||||
case EquipSlot.BodyHands:
|
||||
case EquipSlot.BodyHandsLegsFeet:
|
||||
case EquipSlot.BodyLegsFeet:
|
||||
case EquipSlot.FullBody:
|
||||
case EquipSlot.HeadBody:
|
||||
case EquipSlot.LegsFeet:
|
||||
Add( equipment, EquipmentKey( item ), item );
|
||||
break;
|
||||
default: continue;
|
||||
}
|
||||
}
|
||||
|
||||
_actions = new Dictionary< string, HashSet< Action > >();
|
||||
foreach( var action in dataManager.GetExcelSheet< Action >( clientLanguage )!
|
||||
.Where( a => a.Name.ToString().Any() ) )
|
||||
{
|
||||
var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToString() ?? string.Empty;
|
||||
var endKey = action.AnimationEnd?.Value?.Key.ToString() ?? string.Empty;
|
||||
var hitKey = action.ActionTimelineHit?.Value?.Key.ToString() ?? string.Empty;
|
||||
AddAction( startKey, action );
|
||||
AddAction( endKey, action );
|
||||
AddAction( hitKey, action );
|
||||
}
|
||||
|
||||
_weapons = weapons.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList();
|
||||
_equipment = equipment.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList();
|
||||
}
|
||||
|
||||
private class Comparer : IComparer< (ulong, HashSet< Item >) >
|
||||
{
|
||||
public int Compare( (ulong, HashSet< Item >) x, (ulong, HashSet< Item >) y )
|
||||
=> x.Item1.CompareTo( y.Item1 );
|
||||
}
|
||||
|
||||
private static (int, int) FindIndexRange( List< (ulong, HashSet< Item >) > list, ulong key, ulong mask )
|
||||
{
|
||||
var maskedKey = key & mask;
|
||||
var idx = list.BinarySearch( 0, list.Count, ( key, null! ), new Comparer() );
|
||||
if( idx < 0 )
|
||||
{
|
||||
if( ~idx == list.Count || maskedKey != ( list[ ~idx ].Item1 & mask ) )
|
||||
{
|
||||
return ( -1, -1 );
|
||||
}
|
||||
|
||||
idx = ~idx;
|
||||
}
|
||||
|
||||
var endIdx = idx + 1;
|
||||
while( endIdx < list.Count && maskedKey == ( list[ endIdx ].Item1 & mask ) )
|
||||
{
|
||||
++endIdx;
|
||||
}
|
||||
|
||||
return ( idx, endIdx );
|
||||
}
|
||||
|
||||
private void FindEquipment( IDictionary< string, object? > set, GameObjectInfo info )
|
||||
{
|
||||
var key = ( ulong )info.PrimaryId << 32;
|
||||
var mask = 0xFFFF00000000ul;
|
||||
if( info.EquipSlot != EquipSlot.Unknown )
|
||||
{
|
||||
key |= ( ulong )info.EquipSlot.ToSlot() << 16;
|
||||
mask |= 0xFFFF0000;
|
||||
}
|
||||
|
||||
if( info.Variant != 0 )
|
||||
{
|
||||
key |= info.Variant;
|
||||
mask |= 0xFFFF;
|
||||
}
|
||||
|
||||
var (start, end) = FindIndexRange( _equipment, key, mask );
|
||||
if( start == -1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for( ; start < end; ++start )
|
||||
{
|
||||
foreach( var item in _equipment[ start ].Item2 )
|
||||
{
|
||||
set[ item.Name.ToString() ] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FindWeapon( IDictionary< string, object? > set, GameObjectInfo info )
|
||||
{
|
||||
var key = ( ulong )info.PrimaryId << 32;
|
||||
var mask = 0xFFFF00000000ul;
|
||||
if( info.SecondaryId != 0 )
|
||||
{
|
||||
key |= ( ulong )info.SecondaryId << 16;
|
||||
mask |= 0xFFFF0000;
|
||||
}
|
||||
|
||||
if( info.Variant != 0 )
|
||||
{
|
||||
key |= info.Variant;
|
||||
mask |= 0xFFFF;
|
||||
}
|
||||
|
||||
var (start, end) = FindIndexRange( _weapons, key, mask );
|
||||
if( start == -1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for( ; start < end; ++start )
|
||||
{
|
||||
foreach( var item in _weapons[ start ].Item2 )
|
||||
{
|
||||
set[ item.Name.ToString() ] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCounterString( IDictionary< string, object? > set, string data )
|
||||
{
|
||||
if( set.TryGetValue( data, out var obj ) && obj is int counter )
|
||||
{
|
||||
set[ data ] = counter + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
set[ data ] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void IdentifyParsed( IDictionary< string, object? > set, GameObjectInfo info )
|
||||
{
|
||||
switch( info.ObjectType )
|
||||
{
|
||||
case ObjectType.Unknown:
|
||||
switch( info.FileType )
|
||||
{
|
||||
case FileType.Sound:
|
||||
AddCounterString( set, FileType.Sound.ToString() );
|
||||
break;
|
||||
case FileType.Animation:
|
||||
case FileType.Pap:
|
||||
AddCounterString( set, FileType.Animation.ToString() );
|
||||
break;
|
||||
case FileType.Shader:
|
||||
AddCounterString( set, FileType.Shader.ToString() );
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case ObjectType.LoadingScreen:
|
||||
case ObjectType.Map:
|
||||
case ObjectType.Interface:
|
||||
case ObjectType.Vfx:
|
||||
case ObjectType.World:
|
||||
case ObjectType.Housing:
|
||||
case ObjectType.Font:
|
||||
AddCounterString( set, info.ObjectType.ToString() );
|
||||
break;
|
||||
case ObjectType.DemiHuman:
|
||||
set[ $"Demi Human: {info.PrimaryId}" ] = null;
|
||||
break;
|
||||
case ObjectType.Monster:
|
||||
set[ $"Monster: {info.PrimaryId}" ] = null;
|
||||
break;
|
||||
case ObjectType.Icon:
|
||||
set[ $"Icon: {info.IconId}" ] = null;
|
||||
break;
|
||||
case ObjectType.Accessory:
|
||||
case ObjectType.Equipment:
|
||||
FindEquipment( set, info );
|
||||
break;
|
||||
case ObjectType.Weapon:
|
||||
FindWeapon( set, info );
|
||||
break;
|
||||
case ObjectType.Character:
|
||||
var (gender, race) = info.GenderRace.Split();
|
||||
var raceString = race != ModelRace.Unknown ? race.ToName() + " " : "";
|
||||
var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player ";
|
||||
switch( info.CustomizationType )
|
||||
{
|
||||
case CustomizationType.Skin:
|
||||
set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null;
|
||||
break;
|
||||
case CustomizationType.DecalFace:
|
||||
set[ $"Customization: Face Decal {info.PrimaryId}" ] = null;
|
||||
break;
|
||||
case CustomizationType.Iris when race == ModelRace.Unknown:
|
||||
set[ $"Customization: All Eyes (Catchlight)" ] = null;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
var customizationString = race == ModelRace.Unknown
|
||||
|| info.BodySlot == BodySlot.Unknown
|
||||
|| info.CustomizationType == CustomizationType.Unknown
|
||||
? "Customization: Unknown"
|
||||
: $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
|
||||
set[ customizationString ] = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default: throw new InvalidEnumArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private void IdentifyVfx( IDictionary< string, object? > set, GamePath path )
|
||||
{
|
||||
var key = GameData.GamePathParser.VfxToKey( path );
|
||||
if( key.Length == 0 || !_actions.TryGetValue( key, out var actions ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var action in actions )
|
||||
{
|
||||
set[ $"Action: {action.Name}" ] = action;
|
||||
}
|
||||
}
|
||||
|
||||
public void Identify( IDictionary< string, object? > set, GamePath path )
|
||||
{
|
||||
if( ( ( string )path ).EndsWith( ".pap" ) || ( ( string )path ).EndsWith( ".tmb" ) )
|
||||
{
|
||||
IdentifyVfx( set, path );
|
||||
}
|
||||
else
|
||||
{
|
||||
var info = GameData.GamePathParser.GetFileInfo( path );
|
||||
IdentifyParsed( set, info );
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary< string, object? > Identify( GamePath path )
|
||||
{
|
||||
Dictionary< string, object? > ret = new();
|
||||
Identify( ret, path );
|
||||
return ret;
|
||||
}
|
||||
|
||||
public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot )
|
||||
{
|
||||
switch( slot )
|
||||
{
|
||||
case EquipSlot.MainHand:
|
||||
case EquipSlot.OffHand:
|
||||
{
|
||||
var (begin, _) = FindIndexRange( _weapons, ( ( ulong )setId << 32 ) | ( ( ulong )weaponType << 16 ) | variant,
|
||||
0xFFFFFFFFFFFF );
|
||||
return begin >= 0 ? _weapons[ begin ].Item2.FirstOrDefault() : null;
|
||||
}
|
||||
default:
|
||||
{
|
||||
var (begin, _) = FindIndexRange( _equipment,
|
||||
( ( ulong )setId << 32 ) | ( ( ulong )slot.ToSlot() << 16 ) | variant,
|
||||
0xFFFFFFFFFFFF );
|
||||
return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AssemblyTitle>Penumbra.GameData</AssemblyTitle>
|
||||
<Company>absolute gangstas</Company>
|
||||
<Product>Penumbra</Product>
|
||||
<Copyright>Copyright © 2020</Copyright>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3">
|
||||
<Private>false</Private>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[StructLayout( LayoutKind.Explicit, Pack = 1 )]
|
||||
public readonly struct CharacterArmor
|
||||
{
|
||||
[FieldOffset( 0 )]
|
||||
public readonly SetId Set;
|
||||
|
||||
[FieldOffset( 2 )]
|
||||
public readonly byte Variant;
|
||||
|
||||
[FieldOffset( 3 )]
|
||||
public readonly StainId Stain;
|
||||
|
||||
[FieldOffset( 0 )]
|
||||
public readonly uint Value;
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Set},{Variant},{Stain}";
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
// Read the customization data regarding weapons and displayable equipment from an actor struct.
|
||||
// Stores the data in a 56 bytes, i.e. 7 longs for easier comparison.
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
|
||||
public class CharacterEquipment
|
||||
{
|
||||
public const int MainWeaponOffset = 0x6D0;
|
||||
public const int OffWeaponOffset = 0x738;
|
||||
public const int EquipmentOffset = 0x808;
|
||||
public const int EquipmentSlots = 10;
|
||||
public const int WeaponSlots = 2;
|
||||
|
||||
public CharacterWeapon MainHand;
|
||||
public CharacterWeapon OffHand;
|
||||
public CharacterArmor Head;
|
||||
public CharacterArmor Body;
|
||||
public CharacterArmor Hands;
|
||||
public CharacterArmor Legs;
|
||||
public CharacterArmor Feet;
|
||||
public CharacterArmor Ears;
|
||||
public CharacterArmor Neck;
|
||||
public CharacterArmor Wrists;
|
||||
public CharacterArmor RFinger;
|
||||
public CharacterArmor LFinger;
|
||||
public ushort IsSet; // Also fills struct size to 56, a multiple of 8.
|
||||
|
||||
public CharacterEquipment()
|
||||
=> Clear();
|
||||
|
||||
public CharacterEquipment( Character actor )
|
||||
: this( actor.Address )
|
||||
{ }
|
||||
|
||||
public override string ToString()
|
||||
=> IsSet == 0
|
||||
? "(Not Set)"
|
||||
: $"({MainHand}) | ({OffHand}) | ({Head}) | ({Body}) | ({Hands}) | ({Legs}) | "
|
||||
+ $"({Feet}) | ({Ears}) | ({Neck}) | ({Wrists}) | ({LFinger}) | ({RFinger})";
|
||||
|
||||
public bool Equal( Character rhs )
|
||||
=> CompareData( new CharacterEquipment( rhs ) );
|
||||
|
||||
public bool Equal( CharacterEquipment rhs )
|
||||
=> CompareData( rhs );
|
||||
|
||||
public bool CompareAndUpdate( Character rhs )
|
||||
=> CompareAndOverwrite( new CharacterEquipment( rhs ) );
|
||||
|
||||
public bool CompareAndUpdate( CharacterEquipment rhs )
|
||||
=> CompareAndOverwrite( rhs );
|
||||
|
||||
private unsafe CharacterEquipment( IntPtr actorAddress )
|
||||
{
|
||||
IsSet = 1;
|
||||
var actorPtr = ( byte* )actorAddress.ToPointer();
|
||||
fixed( CharacterWeapon* main = &MainHand, off = &OffHand )
|
||||
{
|
||||
Buffer.MemoryCopy( actorPtr + MainWeaponOffset, main, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) );
|
||||
Buffer.MemoryCopy( actorPtr + OffWeaponOffset, off, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) );
|
||||
}
|
||||
|
||||
fixed( CharacterArmor* equipment = &Head )
|
||||
{
|
||||
Buffer.MemoryCopy( actorPtr + EquipmentOffset, equipment, EquipmentSlots * sizeof( CharacterArmor ),
|
||||
EquipmentSlots * sizeof( CharacterArmor ) );
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void Clear()
|
||||
{
|
||||
fixed( CharacterWeapon* main = &MainHand )
|
||||
{
|
||||
var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8;
|
||||
for( ulong* ptr = ( ulong* )main, end = ptr + structSizeEights; ptr != end; ++ptr )
|
||||
{
|
||||
*ptr = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndOverwrite( CharacterEquipment rhs )
|
||||
{
|
||||
var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8;
|
||||
var ret = true;
|
||||
fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand )
|
||||
{
|
||||
var ptr1 = ( ulong* )data1;
|
||||
var ptr2 = ( ulong* )data2;
|
||||
for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 )
|
||||
{
|
||||
if( *ptr1 != *ptr2 )
|
||||
{
|
||||
*ptr1 = *ptr2;
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private unsafe bool CompareData( CharacterEquipment rhs )
|
||||
{
|
||||
var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8;
|
||||
fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand )
|
||||
{
|
||||
var ptr1 = ( ulong* )data1;
|
||||
var ptr2 = ( ulong* )data2;
|
||||
for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 )
|
||||
{
|
||||
if( *ptr1 != *ptr2 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe void WriteBytes( byte[] array, int offset = 0 )
|
||||
{
|
||||
fixed( CharacterWeapon* data = &MainHand )
|
||||
{
|
||||
Marshal.Copy( new IntPtr( data ), array, offset, 56 );
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var ret = new byte[56];
|
||||
WriteBytes( ret );
|
||||
return ret;
|
||||
}
|
||||
|
||||
public unsafe void FromBytes( byte[] array, int offset = 0 )
|
||||
{
|
||||
fixed( CharacterWeapon* data = &MainHand )
|
||||
{
|
||||
Marshal.Copy( array, offset, new IntPtr( data ), 56 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
{
|
||||
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
|
||||
public readonly struct CharacterWeapon
|
||||
{
|
||||
public readonly SetId Set;
|
||||
public readonly WeaponType Type;
|
||||
public readonly ushort Variant;
|
||||
public readonly StainId Stain;
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Set},{Type},{Variant},{Stain}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[Flags]
|
||||
public enum EqdpEntry : ushort
|
||||
{
|
||||
Invalid = 0,
|
||||
Head1 = 0b0000000001,
|
||||
Head2 = 0b0000000010,
|
||||
HeadMask = 0b0000000011,
|
||||
|
||||
Body1 = 0b0000000100,
|
||||
Body2 = 0b0000001000,
|
||||
BodyMask = 0b0000001100,
|
||||
|
||||
Hands1 = 0b0000010000,
|
||||
Hands2 = 0b0000100000,
|
||||
HandsMask = 0b0000110000,
|
||||
|
||||
Legs1 = 0b0001000000,
|
||||
Legs2 = 0b0010000000,
|
||||
LegsMask = 0b0011000000,
|
||||
|
||||
Feet1 = 0b0100000000,
|
||||
Feet2 = 0b1000000000,
|
||||
FeetMask = 0b1100000000,
|
||||
|
||||
Ears1 = 0b0000000001,
|
||||
Ears2 = 0b0000000010,
|
||||
EarsMask = 0b0000000011,
|
||||
|
||||
Neck1 = 0b0000000100,
|
||||
Neck2 = 0b0000001000,
|
||||
NeckMask = 0b0000001100,
|
||||
|
||||
Wrists1 = 0b0000010000,
|
||||
Wrists2 = 0b0000100000,
|
||||
WristsMask = 0b0000110000,
|
||||
|
||||
RingR1 = 0b0001000000,
|
||||
RingR2 = 0b0010000000,
|
||||
RingRMask = 0b0011000000,
|
||||
|
||||
RingL1 = 0b0100000000,
|
||||
RingL2 = 0b1000000000,
|
||||
RingLMask = 0b1100000000,
|
||||
}
|
||||
|
||||
public static class Eqdp
|
||||
{
|
||||
public static int Offset( EquipSlot slot )
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.Head => 0,
|
||||
EquipSlot.Body => 2,
|
||||
EquipSlot.Hands => 4,
|
||||
EquipSlot.Legs => 6,
|
||||
EquipSlot.Feet => 8,
|
||||
EquipSlot.Ears => 0,
|
||||
EquipSlot.Neck => 2,
|
||||
EquipSlot.Wrists => 4,
|
||||
EquipSlot.RFinger => 6,
|
||||
EquipSlot.LFinger => 8,
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
|
||||
public static (bool, bool) ToBits( this EqdpEntry entry, EquipSlot slot )
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ),
|
||||
EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ),
|
||||
EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ),
|
||||
EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ),
|
||||
EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ),
|
||||
EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ),
|
||||
EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ),
|
||||
EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ),
|
||||
EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ),
|
||||
EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ),
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
|
||||
public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 )
|
||||
{
|
||||
EqdpEntry ret = 0;
|
||||
var offset = Offset( slot );
|
||||
if( bit1 )
|
||||
{
|
||||
ret |= ( EqdpEntry )( 1 << offset );
|
||||
}
|
||||
|
||||
if( bit2 )
|
||||
{
|
||||
ret |= ( EqdpEntry )( 1 << ( offset + 1 ) );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static EqdpEntry Mask( EquipSlot slot )
|
||||
{
|
||||
return slot switch
|
||||
{
|
||||
EquipSlot.Head => EqdpEntry.HeadMask,
|
||||
EquipSlot.Body => EqdpEntry.BodyMask,
|
||||
EquipSlot.Hands => EqdpEntry.HandsMask,
|
||||
EquipSlot.Legs => EqdpEntry.LegsMask,
|
||||
EquipSlot.Feet => EqdpEntry.FeetMask,
|
||||
EquipSlot.Ears => EqdpEntry.EarsMask,
|
||||
EquipSlot.Neck => EqdpEntry.NeckMask,
|
||||
EquipSlot.Wrists => EqdpEntry.WristsMask,
|
||||
EquipSlot.RFinger => EqdpEntry.RingRMask,
|
||||
EquipSlot.LFinger => EqdpEntry.RingLMask,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.ComponentModel;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[Flags]
|
||||
public enum EqpEntry : ulong
|
||||
{
|
||||
BodyEnabled = 0x00_01ul,
|
||||
BodyHideWaist = 0x00_02ul,
|
||||
BodyHideThighs = 0x00_04ul,
|
||||
BodyHideGlovesS = 0x00_08ul,
|
||||
_4 = 0x00_10ul,
|
||||
BodyHideGlovesM = 0x00_20ul,
|
||||
BodyHideGlovesL = 0x00_40ul,
|
||||
BodyHideGorget = 0x00_80ul,
|
||||
BodyShowLeg = 0x01_00ul,
|
||||
BodyShowHand = 0x02_00ul,
|
||||
BodyShowHead = 0x04_00ul,
|
||||
BodyShowNecklace = 0x08_00ul,
|
||||
BodyShowBracelet = 0x10_00ul,
|
||||
BodyShowTail = 0x20_00ul,
|
||||
DisableBreastPhysics = 0x40_00ul,
|
||||
_15 = 0x80_00ul,
|
||||
BodyMask = 0xFF_FFul,
|
||||
|
||||
LegsEnabled = 0x01ul << 16,
|
||||
LegsHideKneePads = 0x02ul << 16,
|
||||
LegsHideBootsS = 0x04ul << 16,
|
||||
LegsHideBootsM = 0x08ul << 16,
|
||||
_20 = 0x10ul << 16,
|
||||
LegsShowFoot = 0x20ul << 16,
|
||||
LegsShowTail = 0x40ul << 16,
|
||||
_23 = 0x80ul << 16,
|
||||
LegsMask = 0xFFul << 16,
|
||||
|
||||
HandsEnabled = 0x01ul << 24,
|
||||
HandsHideElbow = 0x02ul << 24,
|
||||
HandsHideForearm = 0x04ul << 24,
|
||||
_27 = 0x08ul << 24,
|
||||
HandShowBracelet = 0x10ul << 24,
|
||||
HandShowRingL = 0x20ul << 24,
|
||||
HandShowRingR = 0x40ul << 24,
|
||||
_31 = 0x80ul << 24,
|
||||
HandsMask = 0xFFul << 24,
|
||||
|
||||
FeetEnabled = 0x01ul << 32,
|
||||
FeetHideKnee = 0x02ul << 32,
|
||||
FeetHideCalf = 0x04ul << 32,
|
||||
FeetHideAnkle = 0x08ul << 32,
|
||||
_36 = 0x10ul << 32,
|
||||
_37 = 0x20ul << 32,
|
||||
_38 = 0x40ul << 32,
|
||||
_39 = 0x80ul << 32,
|
||||
FeetMask = 0xFFul << 32,
|
||||
|
||||
HeadEnabled = 0x00_00_01ul << 40,
|
||||
HeadHideScalp = 0x00_00_02ul << 40,
|
||||
HeadHideHair = 0x00_00_04ul << 40,
|
||||
HeadShowHairOverride = 0x00_00_08ul << 40,
|
||||
HeadHideNeck = 0x00_00_10ul << 40,
|
||||
HeadShowNecklace = 0x00_00_20ul << 40,
|
||||
_46 = 0x00_00_40ul << 40,
|
||||
HeadShowEarrings = 0x00_00_80ul << 40,
|
||||
HeadShowEarringsHuman = 0x00_01_00ul << 40,
|
||||
HeadShowEarringsAura = 0x00_02_00ul << 40,
|
||||
HeadShowEarHuman = 0x00_04_00ul << 40,
|
||||
HeadShowEarMiqote = 0x00_08_00ul << 40,
|
||||
HeadShowEarAuRa = 0x00_10_00ul << 40,
|
||||
HeadShowEarViera = 0x00_20_00ul << 40,
|
||||
_54 = 0x00_40_00ul << 40,
|
||||
_55 = 0x00_80_00ul << 40,
|
||||
HeadShowHrothgarHat = 0x01_00_00ul << 40,
|
||||
HeadShowVieraHat = 0x02_00_00ul << 40,
|
||||
_58 = 0x04_00_00ul << 40,
|
||||
_59 = 0x08_00_00ul << 40,
|
||||
_60 = 0x10_00_00ul << 40,
|
||||
_61 = 0x20_00_00ul << 40,
|
||||
_62 = 0x40_00_00ul << 40,
|
||||
_63 = 0x80_00_00ul << 40,
|
||||
HeadMask = 0xFF_FF_FFul << 40,
|
||||
}
|
||||
|
||||
public static class Eqp
|
||||
{
|
||||
// cf. Client::Graphics::Scene::CharacterUtility.GetSlotEqpFlags
|
||||
public const EqpEntry DefaultEntry = ( EqpEntry )0x3fe00070603f00;
|
||||
|
||||
public static (int, int) BytesAndOffset( EquipSlot slot )
|
||||
{
|
||||
return slot switch
|
||||
{
|
||||
EquipSlot.Body => ( 2, 0 ),
|
||||
EquipSlot.Legs => ( 1, 2 ),
|
||||
EquipSlot.Hands => ( 1, 3 ),
|
||||
EquipSlot.Feet => ( 1, 4 ),
|
||||
EquipSlot.Head => ( 3, 5 ),
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static EqpEntry FromSlotAndBytes( EquipSlot slot, byte[] value )
|
||||
{
|
||||
EqpEntry ret = 0;
|
||||
var (bytes, offset) = BytesAndOffset( slot );
|
||||
if( bytes != value.Length )
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
for( var i = 0; i < bytes; ++i )
|
||||
{
|
||||
ret |= ( EqpEntry )( ( ulong )value[ i ] << ( ( offset + i ) * 8 ) );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static EqpEntry Mask( EquipSlot slot )
|
||||
{
|
||||
return slot switch
|
||||
{
|
||||
EquipSlot.Body => EqpEntry.BodyMask,
|
||||
EquipSlot.Head => EqpEntry.HeadMask,
|
||||
EquipSlot.Legs => EqpEntry.LegsMask,
|
||||
EquipSlot.Feet => EqpEntry.FeetMask,
|
||||
EquipSlot.Hands => EqpEntry.HandsMask,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
public static EquipSlot ToEquipSlot( this EqpEntry entry )
|
||||
{
|
||||
return entry switch
|
||||
{
|
||||
EqpEntry.BodyEnabled => EquipSlot.Body,
|
||||
EqpEntry.BodyHideWaist => EquipSlot.Body,
|
||||
EqpEntry.BodyHideThighs => EquipSlot.Body,
|
||||
EqpEntry.BodyHideGlovesS => EquipSlot.Body,
|
||||
EqpEntry._4 => EquipSlot.Body,
|
||||
EqpEntry.BodyHideGlovesM => EquipSlot.Body,
|
||||
EqpEntry.BodyHideGlovesL => EquipSlot.Body,
|
||||
EqpEntry.BodyHideGorget => EquipSlot.Body,
|
||||
EqpEntry.BodyShowLeg => EquipSlot.Body,
|
||||
EqpEntry.BodyShowHand => EquipSlot.Body,
|
||||
EqpEntry.BodyShowHead => EquipSlot.Body,
|
||||
EqpEntry.BodyShowNecklace => EquipSlot.Body,
|
||||
EqpEntry.BodyShowBracelet => EquipSlot.Body,
|
||||
EqpEntry.BodyShowTail => EquipSlot.Body,
|
||||
EqpEntry.DisableBreastPhysics => EquipSlot.Body,
|
||||
EqpEntry._15 => EquipSlot.Body,
|
||||
|
||||
EqpEntry.LegsEnabled => EquipSlot.Legs,
|
||||
EqpEntry.LegsHideKneePads => EquipSlot.Legs,
|
||||
EqpEntry.LegsHideBootsS => EquipSlot.Legs,
|
||||
EqpEntry.LegsHideBootsM => EquipSlot.Legs,
|
||||
EqpEntry._20 => EquipSlot.Legs,
|
||||
EqpEntry.LegsShowFoot => EquipSlot.Legs,
|
||||
EqpEntry.LegsShowTail => EquipSlot.Legs,
|
||||
EqpEntry._23 => EquipSlot.Legs,
|
||||
|
||||
EqpEntry.HandsEnabled => EquipSlot.Hands,
|
||||
EqpEntry.HandsHideElbow => EquipSlot.Hands,
|
||||
EqpEntry.HandsHideForearm => EquipSlot.Hands,
|
||||
EqpEntry._27 => EquipSlot.Hands,
|
||||
EqpEntry.HandShowBracelet => EquipSlot.Hands,
|
||||
EqpEntry.HandShowRingL => EquipSlot.Hands,
|
||||
EqpEntry.HandShowRingR => EquipSlot.Hands,
|
||||
EqpEntry._31 => EquipSlot.Hands,
|
||||
|
||||
EqpEntry.FeetEnabled => EquipSlot.Feet,
|
||||
EqpEntry.FeetHideKnee => EquipSlot.Feet,
|
||||
EqpEntry.FeetHideCalf => EquipSlot.Feet,
|
||||
EqpEntry.FeetHideAnkle => EquipSlot.Feet,
|
||||
EqpEntry._36 => EquipSlot.Feet,
|
||||
EqpEntry._37 => EquipSlot.Feet,
|
||||
EqpEntry._38 => EquipSlot.Feet,
|
||||
EqpEntry._39 => EquipSlot.Feet,
|
||||
|
||||
EqpEntry.HeadEnabled => EquipSlot.Head,
|
||||
EqpEntry.HeadHideScalp => EquipSlot.Head,
|
||||
EqpEntry.HeadHideHair => EquipSlot.Head,
|
||||
EqpEntry.HeadShowHairOverride => EquipSlot.Head,
|
||||
EqpEntry.HeadHideNeck => EquipSlot.Head,
|
||||
EqpEntry.HeadShowNecklace => EquipSlot.Head,
|
||||
EqpEntry._46 => EquipSlot.Head,
|
||||
EqpEntry.HeadShowEarrings => EquipSlot.Head,
|
||||
EqpEntry.HeadShowEarringsHuman => EquipSlot.Head,
|
||||
EqpEntry.HeadShowEarringsAura => EquipSlot.Head,
|
||||
EqpEntry.HeadShowEarHuman => EquipSlot.Head,
|
||||
EqpEntry.HeadShowEarMiqote => EquipSlot.Head,
|
||||
EqpEntry.HeadShowEarAuRa => EquipSlot.Head,
|
||||
EqpEntry.HeadShowEarViera => EquipSlot.Head,
|
||||
EqpEntry._54 => EquipSlot.Head,
|
||||
EqpEntry._55 => EquipSlot.Head,
|
||||
EqpEntry.HeadShowHrothgarHat => EquipSlot.Head,
|
||||
EqpEntry.HeadShowVieraHat => EquipSlot.Head,
|
||||
|
||||
// Currently unused.
|
||||
EqpEntry._58 => EquipSlot.Unknown,
|
||||
EqpEntry._59 => EquipSlot.Unknown,
|
||||
EqpEntry._60 => EquipSlot.Unknown,
|
||||
EqpEntry._61 => EquipSlot.Unknown,
|
||||
EqpEntry._62 => EquipSlot.Unknown,
|
||||
EqpEntry._63 => EquipSlot.Unknown,
|
||||
|
||||
_ => EquipSlot.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToLocalName( this EqpEntry entry )
|
||||
{
|
||||
return entry switch
|
||||
{
|
||||
EqpEntry.BodyEnabled => "Enabled",
|
||||
EqpEntry.BodyHideWaist => "Hide Waist",
|
||||
EqpEntry.BodyHideThighs => "Hide Thigh Pads",
|
||||
EqpEntry.BodyHideGlovesS => "Hide Small Gloves",
|
||||
EqpEntry._4 => "Unknown 4",
|
||||
EqpEntry.BodyHideGlovesM => "Hide Medium Gloves",
|
||||
EqpEntry.BodyHideGlovesL => "Hide Large Gloves",
|
||||
EqpEntry.BodyHideGorget => "Hide Gorget",
|
||||
EqpEntry.BodyShowLeg => "Show Legs",
|
||||
EqpEntry.BodyShowHand => "Show Hands",
|
||||
EqpEntry.BodyShowHead => "Show Head",
|
||||
EqpEntry.BodyShowNecklace => "Show Necklace",
|
||||
EqpEntry.BodyShowBracelet => "Show Bracelet",
|
||||
EqpEntry.BodyShowTail => "Show Tail",
|
||||
EqpEntry.DisableBreastPhysics => "Disable Breast Physics",
|
||||
EqpEntry._15 => "Unknown 15",
|
||||
|
||||
EqpEntry.LegsEnabled => "Enabled",
|
||||
EqpEntry.LegsHideKneePads => "Hide Knee Pads",
|
||||
EqpEntry.LegsHideBootsS => "Hide Small Boots",
|
||||
EqpEntry.LegsHideBootsM => "Hide Medium Boots",
|
||||
EqpEntry._20 => "Unknown 20",
|
||||
EqpEntry.LegsShowFoot => "Show Foot",
|
||||
EqpEntry.LegsShowTail => "Show Tail",
|
||||
EqpEntry._23 => "Unknown 23",
|
||||
|
||||
EqpEntry.HandsEnabled => "Enabled",
|
||||
EqpEntry.HandsHideElbow => "Hide Elbow",
|
||||
EqpEntry.HandsHideForearm => "Hide Forearm",
|
||||
EqpEntry._27 => "Unknown 27",
|
||||
EqpEntry.HandShowBracelet => "Show Bracelet",
|
||||
EqpEntry.HandShowRingL => "Show Left Ring",
|
||||
EqpEntry.HandShowRingR => "Show Right Ring",
|
||||
EqpEntry._31 => "Unknown 31",
|
||||
|
||||
EqpEntry.FeetEnabled => "Enabled",
|
||||
EqpEntry.FeetHideKnee => "Hide Knees",
|
||||
EqpEntry.FeetHideCalf => "Hide Calves",
|
||||
EqpEntry.FeetHideAnkle => "Hide Ankles",
|
||||
EqpEntry._36 => "Unknown 36",
|
||||
EqpEntry._37 => "Unknown 37",
|
||||
EqpEntry._38 => "Unknown 38",
|
||||
EqpEntry._39 => "Unknown 39",
|
||||
|
||||
EqpEntry.HeadEnabled => "Enabled",
|
||||
EqpEntry.HeadHideScalp => "Hide Scalp",
|
||||
EqpEntry.HeadHideHair => "Hide Hair",
|
||||
EqpEntry.HeadShowHairOverride => "Show Hair Override",
|
||||
EqpEntry.HeadHideNeck => "Hide Neck",
|
||||
EqpEntry.HeadShowNecklace => "Show Necklace",
|
||||
EqpEntry._46 => "Unknown 46",
|
||||
EqpEntry.HeadShowEarrings => "Show Earrings",
|
||||
EqpEntry.HeadShowEarringsHuman => "Show Earrings (Human)",
|
||||
EqpEntry.HeadShowEarringsAura => "Show Earrings (Au Ra)",
|
||||
EqpEntry.HeadShowEarHuman => "Show Ears (Human)",
|
||||
EqpEntry.HeadShowEarMiqote => "Show Ears (Miqo'te)",
|
||||
EqpEntry.HeadShowEarAuRa => "Show Ears (Au Ra)",
|
||||
EqpEntry.HeadShowEarViera => "Show Ears (Viera)",
|
||||
EqpEntry._54 => "Unknown 54",
|
||||
EqpEntry._55 => "Unknown 55",
|
||||
EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar",
|
||||
EqpEntry.HeadShowVieraHat => "Show on Viera",
|
||||
EqpEntry._58 => "Unknown 58",
|
||||
EqpEntry._59 => "Unknown 59",
|
||||
EqpEntry._60 => "Unknown 60",
|
||||
EqpEntry._61 => "Unknown 61",
|
||||
EqpEntry._62 => "Unknown 62",
|
||||
EqpEntry._63 => "Unknown 63",
|
||||
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
private static EqpEntry[] GetEntriesForSlot( EquipSlot slot )
|
||||
{
|
||||
return ( ( EqpEntry[] )Enum.GetValues( typeof( EqpEntry ) ) )
|
||||
.Where( e => e.ToEquipSlot() == slot )
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot( EquipSlot.Body );
|
||||
public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot( EquipSlot.Legs );
|
||||
public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot( EquipSlot.Hands );
|
||||
public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet );
|
||||
public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head );
|
||||
|
||||
public static readonly IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >()
|
||||
{
|
||||
[ EquipSlot.Body ] = EqpAttributesBody,
|
||||
[ EquipSlot.Legs ] = EqpAttributesLegs,
|
||||
[ EquipSlot.Hands ] = EqpAttributesHands,
|
||||
[ EquipSlot.Feet ] = EqpAttributesFeet,
|
||||
[ EquipSlot.Head ] = EqpAttributesHead,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[StructLayout( LayoutKind.Explicit )]
|
||||
public struct GameObjectInfo : IComparable
|
||||
{
|
||||
public static GameObjectInfo Equipment( FileType type, ushort setId, GenderRace gr = GenderRace.Unknown
|
||||
, EquipSlot slot = EquipSlot.Unknown, byte variant = 0 )
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment,
|
||||
PrimaryId = setId,
|
||||
GenderRace = gr,
|
||||
Variant = variant,
|
||||
EquipSlot = slot,
|
||||
};
|
||||
|
||||
public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 )
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = ObjectType.Weapon,
|
||||
PrimaryId = setId,
|
||||
SecondaryId = weaponId,
|
||||
Variant = variant,
|
||||
};
|
||||
|
||||
public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0
|
||||
, GenderRace gr = GenderRace.Unknown, BodySlot bodySlot = BodySlot.Unknown, byte variant = 0 )
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = ObjectType.Character,
|
||||
PrimaryId = id,
|
||||
GenderRace = gr,
|
||||
BodySlot = bodySlot,
|
||||
Variant = variant,
|
||||
CustomizationType = customizationType,
|
||||
};
|
||||
|
||||
public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 )
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = ObjectType.Monster,
|
||||
PrimaryId = monsterId,
|
||||
SecondaryId = bodyId,
|
||||
Variant = variant,
|
||||
};
|
||||
|
||||
public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, EquipSlot slot = EquipSlot.Unknown,
|
||||
byte variant = 0
|
||||
)
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = ObjectType.DemiHuman,
|
||||
PrimaryId = demiHumanId,
|
||||
SecondaryId = bodyId,
|
||||
Variant = variant,
|
||||
EquipSlot = slot,
|
||||
};
|
||||
|
||||
public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 )
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = ObjectType.Map,
|
||||
MapC1 = c1,
|
||||
MapC2 = c2,
|
||||
MapC3 = c3,
|
||||
MapC4 = c4,
|
||||
MapSuffix = suffix,
|
||||
Variant = variant,
|
||||
};
|
||||
|
||||
public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, bool hr, ClientLanguage lang = ClientLanguage.English )
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = ObjectType.Icon,
|
||||
IconId = iconId,
|
||||
IconHqHr = ( byte )( hq ? hr ? 3 : 1 : hr ? 2 : 0 ),
|
||||
Language = lang,
|
||||
};
|
||||
|
||||
|
||||
[FieldOffset( 0 )]
|
||||
public readonly ulong Identifier;
|
||||
|
||||
[FieldOffset( 0 )]
|
||||
public FileType FileType;
|
||||
|
||||
[FieldOffset( 1 )]
|
||||
public ObjectType ObjectType;
|
||||
|
||||
|
||||
[FieldOffset( 2 )]
|
||||
public ushort PrimaryId; // Equipment, Weapon, Customization, Monster, DemiHuman
|
||||
|
||||
[FieldOffset( 2 )]
|
||||
public uint IconId; // Icon
|
||||
|
||||
[FieldOffset( 2 )]
|
||||
public byte MapC1; // Map
|
||||
|
||||
[FieldOffset( 3 )]
|
||||
public byte MapC2; // Map
|
||||
|
||||
[FieldOffset( 4 )]
|
||||
public ushort SecondaryId; // Weapon, Monster, Demihuman
|
||||
|
||||
[FieldOffset( 4 )]
|
||||
public byte MapC3; // Map
|
||||
|
||||
[FieldOffset( 4 )]
|
||||
private byte _genderRaceByte; // Equipment, Customization
|
||||
|
||||
public GenderRace GenderRace
|
||||
{
|
||||
get => Names.GenderRaceFromByte( _genderRaceByte );
|
||||
set => _genderRaceByte = value.ToByte();
|
||||
}
|
||||
|
||||
[FieldOffset( 5 )]
|
||||
public BodySlot BodySlot; // Customization
|
||||
|
||||
[FieldOffset( 5 )]
|
||||
public byte MapC4; // Map
|
||||
|
||||
[FieldOffset( 6 )]
|
||||
public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman
|
||||
|
||||
[FieldOffset( 6 )]
|
||||
public byte IconHqHr; // Icon
|
||||
|
||||
[FieldOffset( 7 )]
|
||||
public EquipSlot EquipSlot; // Equipment, Demihuman
|
||||
|
||||
[FieldOffset( 7 )]
|
||||
public CustomizationType CustomizationType; // Customization
|
||||
|
||||
[FieldOffset( 7 )]
|
||||
public ClientLanguage Language; // Icon
|
||||
|
||||
[FieldOffset( 7 )]
|
||||
public byte MapSuffix;
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Identifier.GetHashCode();
|
||||
|
||||
public int CompareTo( object? r )
|
||||
=> Identifier.CompareTo( r );
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
public struct GmpEntry : IEquatable< GmpEntry >
|
||||
{
|
||||
public static readonly GmpEntry Default = new();
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => ( Value & 1 ) == 1;
|
||||
set
|
||||
{
|
||||
if( value )
|
||||
{
|
||||
Value |= 1ul;
|
||||
}
|
||||
else
|
||||
{
|
||||
Value &= ~1ul;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Animated
|
||||
{
|
||||
get => ( Value & 2 ) == 2;
|
||||
set
|
||||
{
|
||||
if( value )
|
||||
{
|
||||
Value |= 2ul;
|
||||
}
|
||||
else
|
||||
{
|
||||
Value &= ~2ul;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ushort RotationA
|
||||
{
|
||||
get => ( ushort )( ( Value >> 2 ) & 0x3FF );
|
||||
set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 );
|
||||
}
|
||||
|
||||
public ushort RotationB
|
||||
{
|
||||
get => ( ushort )( ( Value >> 12 ) & 0x3FF );
|
||||
set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 );
|
||||
}
|
||||
|
||||
public ushort RotationC
|
||||
{
|
||||
get => ( ushort )( ( Value >> 22 ) & 0x3FF );
|
||||
set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 );
|
||||
}
|
||||
|
||||
public byte UnknownA
|
||||
{
|
||||
get => ( byte )( ( Value >> 32 ) & 0x0F );
|
||||
set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 );
|
||||
}
|
||||
|
||||
public byte UnknownB
|
||||
{
|
||||
get => ( byte )( ( Value >> 36 ) & 0x0F );
|
||||
set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 );
|
||||
}
|
||||
|
||||
public byte UnknownTotal
|
||||
{
|
||||
get => ( byte )( ( Value >> 32 ) & 0xFF );
|
||||
set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 );
|
||||
}
|
||||
|
||||
public ulong Value { get; set; }
|
||||
|
||||
public static GmpEntry FromTexToolsMeta( byte[] data )
|
||||
{
|
||||
GmpEntry ret = new();
|
||||
using var reader = new BinaryReader( new MemoryStream( data ) );
|
||||
ret.Value = reader.ReadUInt32();
|
||||
ret.UnknownTotal = data[ 4 ];
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static implicit operator ulong( GmpEntry entry )
|
||||
=> entry.Value;
|
||||
|
||||
public static explicit operator GmpEntry( ulong entry )
|
||||
=> new() { Value = entry };
|
||||
|
||||
public bool Equals( GmpEntry other )
|
||||
=> Value == other.Value;
|
||||
|
||||
public override bool Equals( object? obj )
|
||||
=> obj is GmpEntry other && Equals( other );
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Value.GetHashCode();
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
{
|
||||
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
|
||||
public readonly struct RspEntry
|
||||
{
|
||||
public const int ByteSize = ( int )RspAttribute.NumAttributes * 4;
|
||||
|
||||
private readonly float[] Attributes;
|
||||
|
||||
public RspEntry( RspEntry copy )
|
||||
=> Attributes = ( float[] )copy.Attributes.Clone();
|
||||
|
||||
public RspEntry( byte[] bytes, int offset )
|
||||
{
|
||||
if( offset < 0 || offset + ByteSize > bytes.Length )
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
Attributes = new float[( int )RspAttribute.NumAttributes];
|
||||
using MemoryStream s = new( bytes ) { Position = offset };
|
||||
using BinaryReader br = new( s );
|
||||
for( var i = 0; i < ( int )RspAttribute.NumAttributes; ++i )
|
||||
{
|
||||
Attributes[ i ] = br.ReadSingle();
|
||||
}
|
||||
}
|
||||
|
||||
private static int ToIndex( RspAttribute attribute )
|
||||
=> attribute < RspAttribute.NumAttributes && attribute >= 0
|
||||
? ( int )attribute
|
||||
: throw new InvalidEnumArgumentException();
|
||||
|
||||
public float this[ RspAttribute attribute ]
|
||||
{
|
||||
get => Attributes[ ToIndex( attribute ) ];
|
||||
set => Attributes[ ToIndex( attribute ) ] = value;
|
||||
}
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
using var s = new MemoryStream( ByteSize );
|
||||
using var bw = new BinaryWriter( s );
|
||||
foreach( var attribute in Attributes )
|
||||
{
|
||||
bw.Write( attribute );
|
||||
}
|
||||
|
||||
return s.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
{
|
||||
public readonly struct SetId : IComparable< SetId >
|
||||
{
|
||||
public readonly ushort Value;
|
||||
|
||||
public SetId( ushort value )
|
||||
=> Value = value;
|
||||
|
||||
public static implicit operator SetId( ushort id )
|
||||
=> new( id );
|
||||
|
||||
public static explicit operator ushort( SetId id )
|
||||
=> id.Value;
|
||||
|
||||
public override string ToString()
|
||||
=> Value.ToString();
|
||||
|
||||
public int CompareTo( SetId other )
|
||||
=> Value.CompareTo( other.Value );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
{
|
||||
public readonly struct StainId : IEquatable< StainId >
|
||||
{
|
||||
public readonly byte Value;
|
||||
|
||||
public StainId( byte value )
|
||||
=> Value = value;
|
||||
|
||||
public static implicit operator StainId( byte id )
|
||||
=> new( id );
|
||||
|
||||
public static explicit operator byte( StainId id )
|
||||
=> id.Value;
|
||||
|
||||
public override string ToString()
|
||||
=> Value.ToString();
|
||||
|
||||
public bool Equals( StainId other )
|
||||
=> Value == other.Value;
|
||||
|
||||
public override bool Equals( object? obj )
|
||||
=> obj is StainId other && Equals( other );
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
{
|
||||
public readonly struct WeaponType : IEquatable< WeaponType >
|
||||
{
|
||||
public readonly ushort Value;
|
||||
|
||||
public WeaponType( ushort value )
|
||||
=> Value = value;
|
||||
|
||||
public static implicit operator WeaponType( ushort id )
|
||||
=> new( id );
|
||||
|
||||
public static explicit operator ushort( WeaponType id )
|
||||
=> id.Value;
|
||||
|
||||
public override string ToString()
|
||||
=> Value.ToString();
|
||||
|
||||
public bool Equals( WeaponType other )
|
||||
=> Value == other.Value;
|
||||
|
||||
public override bool Equals( object? obj )
|
||||
=> obj is WeaponType other && Equals( other );
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using ByteStringFunctions = Penumbra.GameData.ByteString.ByteStringFunctions;
|
||||
|
||||
namespace Penumbra.GameData.Util;
|
||||
|
||||
public static class Functions
|
||||
{
|
||||
public static ulong ComputeCrc64( string name )
|
||||
{
|
||||
if( name.Length == 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var lastSlash = name.LastIndexOf( '/' );
|
||||
if( lastSlash == -1 )
|
||||
{
|
||||
return Lumina.Misc.Crc32.Get( name );
|
||||
}
|
||||
|
||||
var folder = name[ ..lastSlash ];
|
||||
var file = name[ ( lastSlash + 1 ).. ];
|
||||
return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file );
|
||||
}
|
||||
|
||||
public static ulong ComputeCrc64( ReadOnlySpan< byte > name )
|
||||
{
|
||||
if( name.Length == 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var lastSlash = name.LastIndexOf( ( byte )'/' );
|
||||
if( lastSlash == -1 )
|
||||
{
|
||||
return Lumina.Misc.Crc32.Get( name );
|
||||
}
|
||||
|
||||
var folder = name[ ..lastSlash ];
|
||||
var file = name[ ( lastSlash + 1 ).. ];
|
||||
return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file );
|
||||
}
|
||||
|
||||
private static readonly uint[] CrcTable =
|
||||
typeof( Lumina.Misc.Crc32 ).GetField( "CrcTable", BindingFlags.Static | BindingFlags.NonPublic )?.GetValue( null ) as uint[]
|
||||
?? throw new Exception( "Could not fetch CrcTable from Lumina." );
|
||||
|
||||
|
||||
public static unsafe int ComputeCrc64LowerAndSize( byte* ptr, out ulong crc64, out int crc32Ret, out bool isLower, out bool isAscii )
|
||||
{
|
||||
var tmp = ptr;
|
||||
uint crcFolder = 0;
|
||||
uint crcFile = 0;
|
||||
var crc32 = uint.MaxValue;
|
||||
crc64 = 0;
|
||||
isLower = true;
|
||||
isAscii = true;
|
||||
while( true )
|
||||
{
|
||||
var value = *tmp;
|
||||
if( value == 0 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp )
|
||||
{
|
||||
isLower = false;
|
||||
}
|
||||
|
||||
if( value > 0x80 )
|
||||
{
|
||||
isAscii = false;
|
||||
}
|
||||
|
||||
if( value == ( byte )'/' )
|
||||
{
|
||||
crcFolder = crc32;
|
||||
crcFile = uint.MaxValue;
|
||||
crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 );
|
||||
}
|
||||
else
|
||||
{
|
||||
crcFile = CrcTable[ ( byte )( crcFolder ^ value ) ] ^ ( crcFolder >> 8 );
|
||||
crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 );
|
||||
}
|
||||
|
||||
++tmp;
|
||||
}
|
||||
|
||||
var size = ( int )( tmp - ptr );
|
||||
crc64 = ~( ( ulong )crcFolder << 32 ) | crcFile;
|
||||
crc32Ret = ( int )~crc32;
|
||||
return size;
|
||||
}
|
||||
|
||||
public static unsafe int ComputeCrc32AsciiLowerAndSize( byte* ptr, out int crc32Ret, out bool isLower, out bool isAscii )
|
||||
{
|
||||
var tmp = ptr;
|
||||
var crc32 = uint.MaxValue;
|
||||
isLower = true;
|
||||
isAscii = true;
|
||||
while( true )
|
||||
{
|
||||
var value = *tmp;
|
||||
if( value == 0 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp )
|
||||
{
|
||||
isLower = false;
|
||||
}
|
||||
|
||||
if( value > 0x80 )
|
||||
{
|
||||
isAscii = false;
|
||||
}
|
||||
|
||||
crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 );
|
||||
++tmp;
|
||||
}
|
||||
|
||||
var size = ( int )( tmp - ptr );
|
||||
crc32Ret = ( int )~crc32;
|
||||
return size;
|
||||
}
|
||||
|
||||
[DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )]
|
||||
private static extern unsafe IntPtr memcpy( void* dest, void* src, int count );
|
||||
|
||||
public static unsafe void MemCpyUnchecked( void* dest, void* src, int count )
|
||||
=> memcpy( dest, src, count );
|
||||
|
||||
|
||||
[DllImport( "msvcrt.dll", EntryPoint = "memcmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )]
|
||||
private static extern unsafe int memcmp( void* b1, void* b2, int count );
|
||||
|
||||
public static unsafe int MemCmpUnchecked( void* ptr1, void* ptr2, int count )
|
||||
=> memcmp( ptr1, ptr2, count );
|
||||
|
||||
|
||||
[DllImport( "msvcrt.dll", EntryPoint = "_memicmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )]
|
||||
private static extern unsafe int memicmp( void* b1, void* b2, int count );
|
||||
|
||||
public static unsafe int MemCmpCaseInsensitiveUnchecked( void* ptr1, void* ptr2, int count )
|
||||
=> memicmp( ptr1, ptr2, count );
|
||||
|
||||
[DllImport( "msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false )]
|
||||
private static extern unsafe void* memset( void* dest, int c, int count );
|
||||
|
||||
public static unsafe void* MemSet( void* dest, byte value, int count )
|
||||
=> memset( dest, value, count );
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
|
||||
|
||||
namespace Penumbra.GameData.Util;
|
||||
|
||||
public readonly struct GamePath : IComparable
|
||||
{
|
||||
public const int MaxGamePathLength = 256;
|
||||
|
||||
private readonly string _path;
|
||||
|
||||
private GamePath( string path, bool _ )
|
||||
=> _path = path;
|
||||
|
||||
public GamePath( string? path )
|
||||
{
|
||||
if( path is { Length: < MaxGamePathLength } )
|
||||
{
|
||||
_path = Lower( Trim( ReplaceSlash( path ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
_path = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public GamePath( FileInfo file, DirectoryInfo baseDir )
|
||||
=> _path = CheckPre( file, baseDir ) ? Lower( Trim( ReplaceSlash( Substring( file, baseDir ) ) ) ) : "";
|
||||
|
||||
private static bool CheckPre( FileInfo file, DirectoryInfo baseDir )
|
||||
=> file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxGamePathLength;
|
||||
|
||||
private static string Substring( FileInfo file, DirectoryInfo baseDir )
|
||||
=> file.FullName.Substring( baseDir.FullName.Length );
|
||||
|
||||
private static string ReplaceSlash( string path )
|
||||
=> path.Replace( '\\', '/' );
|
||||
|
||||
private static string Trim( string path )
|
||||
=> path.TrimStart( '/' );
|
||||
|
||||
private static string Lower( string path )
|
||||
=> path.ToLowerInvariant();
|
||||
|
||||
public static GamePath GenerateUnchecked( string path )
|
||||
=> new(path, true);
|
||||
|
||||
public static GamePath GenerateUncheckedLower( string path )
|
||||
=> new(Lower( path ), true);
|
||||
|
||||
public static implicit operator string( GamePath gamePath )
|
||||
=> gamePath._path;
|
||||
|
||||
public static explicit operator GamePath( string gamePath )
|
||||
=> new(gamePath);
|
||||
|
||||
public bool Empty
|
||||
=> _path.Length == 0;
|
||||
|
||||
public string Filename()
|
||||
{
|
||||
var idx = _path.LastIndexOf( "/", StringComparison.Ordinal );
|
||||
return idx == -1 ? _path : idx == _path.Length - 1 ? "" : _path[ ( idx + 1 ).. ];
|
||||
}
|
||||
|
||||
public int CompareTo( object? rhs )
|
||||
{
|
||||
return rhs switch
|
||||
{
|
||||
string path => string.Compare( _path, path, StringComparison.Ordinal ),
|
||||
GamePath path => string.Compare( _path, path._path, StringComparison.Ordinal ),
|
||||
_ => -1,
|
||||
};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> _path;
|
||||
}
|
||||
|
||||
public class GamePathConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( GamePath );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
var token = JToken.Load( reader );
|
||||
return token.ToObject< GamePath >();
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
=> true;
|
||||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
if( value != null )
|
||||
{
|
||||
var v = ( GamePath )value;
|
||||
serializer.Serialize( writer, v.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
namespace Penumbra.PlayerWatch
|
||||
{
|
||||
public static class CharacterFactory
|
||||
{
|
||||
private static ConstructorInfo? _characterConstructor;
|
||||
|
||||
private static void Initialize()
|
||||
{
|
||||
_characterConstructor ??= typeof( Character ).GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, new[]
|
||||
{
|
||||
typeof( IntPtr ),
|
||||
}, null )!;
|
||||
}
|
||||
|
||||
private static Character Character( IntPtr address )
|
||||
{
|
||||
Initialize();
|
||||
return ( Character )_characterConstructor?.Invoke( new object[]
|
||||
{
|
||||
address,
|
||||
} )!;
|
||||
}
|
||||
|
||||
public static Character? Convert( GameObject? actor )
|
||||
{
|
||||
if( actor == null )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return actor switch
|
||||
{
|
||||
PlayerCharacter p => p,
|
||||
BattleChara b => b,
|
||||
_ => actor.ObjectKind switch
|
||||
{
|
||||
ObjectKind.BattleNpc => Character( actor.Address ),
|
||||
ObjectKind.Companion => Character( actor.Address ),
|
||||
ObjectKind.Retainer => Character( actor.Address ),
|
||||
ObjectKind.EventNpc => Character( actor.Address ),
|
||||
_ => null,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class GameObjectExtensions
|
||||
{
|
||||
private const int ModelTypeOffset = 0x01B4;
|
||||
|
||||
public static unsafe int ModelType( this GameObject actor )
|
||||
=> *( int* )( actor.Address + ModelTypeOffset );
|
||||
|
||||
public static unsafe void SetModelType( this GameObject actor, int value )
|
||||
=> *( int* )( actor.Address + ModelTypeOffset ) = value;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Penumbra.PlayerWatch;
|
||||
|
||||
public delegate void PlayerChange( Character actor );
|
||||
|
||||
public interface IPlayerWatcherBase : IDisposable
|
||||
{
|
||||
public int Version { get; }
|
||||
public bool Valid { get; }
|
||||
}
|
||||
|
||||
public interface IPlayerWatcher : IPlayerWatcherBase
|
||||
{
|
||||
public event PlayerChange? PlayerChanged;
|
||||
public bool Active { get; }
|
||||
|
||||
public void Enable();
|
||||
public void Disable();
|
||||
public void SetStatus( bool enabled );
|
||||
|
||||
public void AddPlayerToWatch( string playerName );
|
||||
public void RemovePlayerFromWatch( string playerName );
|
||||
public CharacterEquipment UpdatePlayerWithoutEvent( Character actor );
|
||||
|
||||
public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers();
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AssemblyTitle>Penumbra.PlayerWatch</AssemblyTitle>
|
||||
<Company>absolute gangstas</Company>
|
||||
<Product>Penumbra</Product>
|
||||
<Copyright>Copyright © 2020</Copyright>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Penumbra.PlayerWatch;
|
||||
|
||||
internal readonly struct WatchedPlayer
|
||||
{
|
||||
public readonly Dictionary< ulong, CharacterEquipment > FoundActors;
|
||||
public readonly HashSet< PlayerWatcher > RegisteredWatchers;
|
||||
|
||||
public WatchedPlayer( PlayerWatcher watcher )
|
||||
{
|
||||
FoundActors = new Dictionary< ulong, CharacterEquipment >( 4 );
|
||||
RegisteredWatchers = new HashSet< PlayerWatcher > { watcher };
|
||||
}
|
||||
}
|
||||
|
||||
internal class PlayerWatchBase : IDisposable
|
||||
{
|
||||
public const int GPosePlayerIdx = 201;
|
||||
public const int GPoseTableEnd = GPosePlayerIdx + 40;
|
||||
private const int ObjectsPerFrame = 32;
|
||||
|
||||
private readonly Framework _framework;
|
||||
private readonly ClientState _clientState;
|
||||
private readonly ObjectTable _objects;
|
||||
internal readonly HashSet< PlayerWatcher > RegisteredWatchers = new();
|
||||
internal readonly Dictionary< string, WatchedPlayer > Equip = new();
|
||||
internal HashSet< ulong > SeenActors;
|
||||
private int _frameTicker;
|
||||
private bool _inGPose;
|
||||
private bool _enabled;
|
||||
private bool _cancel;
|
||||
|
||||
internal PlayerWatchBase( Framework framework, ClientState clientState, ObjectTable objects )
|
||||
{
|
||||
_framework = framework;
|
||||
_clientState = clientState;
|
||||
_objects = objects;
|
||||
SeenActors = new HashSet< ulong >( _objects.Length );
|
||||
}
|
||||
|
||||
internal void RegisterWatcher( PlayerWatcher watcher )
|
||||
{
|
||||
RegisteredWatchers.Add( watcher );
|
||||
if( watcher.Active )
|
||||
{
|
||||
EnablePlayerWatch();
|
||||
}
|
||||
}
|
||||
|
||||
internal void UnregisterWatcher( PlayerWatcher watcher )
|
||||
{
|
||||
if( RegisteredWatchers.Remove( watcher ) )
|
||||
{
|
||||
foreach( var (key, value) in Equip.ToArray() )
|
||||
{
|
||||
if( value.RegisteredWatchers.Remove( watcher ) && value.RegisteredWatchers.Count == 0 )
|
||||
{
|
||||
Equip.Remove( key );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckActiveStatus();
|
||||
}
|
||||
|
||||
internal void CheckActiveStatus()
|
||||
{
|
||||
if( RegisteredWatchers.Any( w => w.Active ) )
|
||||
{
|
||||
EnablePlayerWatch();
|
||||
}
|
||||
else
|
||||
{
|
||||
DisablePlayerWatch();
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong GetId( GameObject actor )
|
||||
=> actor.ObjectId | ( ( ulong )actor.OwnerId << 32 );
|
||||
|
||||
internal CharacterEquipment UpdatePlayerWithoutEvent( Character actor )
|
||||
{
|
||||
var name = actor.Name.ToString();
|
||||
var equipment = new CharacterEquipment( actor );
|
||||
if( Equip.TryGetValue( name, out var watched ) )
|
||||
{
|
||||
watched.FoundActors[ GetId( actor ) ] = equipment;
|
||||
}
|
||||
|
||||
return equipment;
|
||||
}
|
||||
|
||||
internal void AddPlayerToWatch( string playerName, PlayerWatcher watcher )
|
||||
{
|
||||
if( Equip.TryGetValue( playerName, out var items ) )
|
||||
{
|
||||
items.RegisteredWatchers.Add( watcher );
|
||||
}
|
||||
else
|
||||
{
|
||||
Equip[ playerName ] = new WatchedPlayer( watcher );
|
||||
}
|
||||
}
|
||||
|
||||
public void RemovePlayerFromWatch( string playerName, PlayerWatcher watcher )
|
||||
{
|
||||
if( Equip.TryGetValue( playerName, out var items ) )
|
||||
{
|
||||
if( items.RegisteredWatchers.Remove( watcher ) && items.RegisteredWatchers.Count == 0 )
|
||||
{
|
||||
Equip.Remove( playerName );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void EnablePlayerWatch()
|
||||
{
|
||||
if( !_enabled )
|
||||
{
|
||||
_enabled = true;
|
||||
_framework.Update += OnFrameworkUpdate;
|
||||
_clientState.TerritoryChanged += OnTerritoryChange;
|
||||
_clientState.Logout += OnLogout;
|
||||
}
|
||||
}
|
||||
|
||||
internal void DisablePlayerWatch()
|
||||
{
|
||||
if( _enabled )
|
||||
{
|
||||
_enabled = false;
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
_clientState.TerritoryChanged -= OnTerritoryChange;
|
||||
_clientState.Logout -= OnLogout;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> DisablePlayerWatch();
|
||||
|
||||
private void OnTerritoryChange( object? _1, ushort _2 )
|
||||
=> Clear();
|
||||
|
||||
private void OnLogout( object? _1, object? _2 )
|
||||
=> Clear();
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
PluginLog.Debug( "Clearing PlayerWatcher Store." );
|
||||
_cancel = true;
|
||||
foreach( var kvp in Equip )
|
||||
{
|
||||
kvp.Value.FoundActors.Clear();
|
||||
}
|
||||
|
||||
_frameTicker = 0;
|
||||
}
|
||||
|
||||
private static void TriggerEvents( IEnumerable< PlayerWatcher > watchers, Character player )
|
||||
{
|
||||
PluginLog.Debug( "Triggering events for {PlayerName} at {Address}.", player.Name, player.Address );
|
||||
foreach( var watcher in watchers.Where( w => w.Active ) )
|
||||
{
|
||||
watcher.Trigger( player );
|
||||
}
|
||||
}
|
||||
|
||||
internal void TriggerGPose()
|
||||
{
|
||||
for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i )
|
||||
{
|
||||
var player = _objects[ i ];
|
||||
if( player == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( Equip.TryGetValue( player.Name.ToString(), out var watcher ) )
|
||||
{
|
||||
TriggerEvents( watcher.RegisteredWatchers, ( Character )player );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Character? CheckGPoseObject( GameObject player )
|
||||
{
|
||||
if( !_inGPose )
|
||||
{
|
||||
return CharacterFactory.Convert( player );
|
||||
}
|
||||
|
||||
for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i )
|
||||
{
|
||||
var a = _objects[ i ];
|
||||
if( a == null )
|
||||
{
|
||||
return CharacterFactory.Convert( player );
|
||||
}
|
||||
|
||||
if( a.Name == player.Name )
|
||||
{
|
||||
return CharacterFactory.Convert( a );
|
||||
}
|
||||
}
|
||||
|
||||
return CharacterFactory.Convert( player )!;
|
||||
}
|
||||
|
||||
private bool TryGetPlayer( GameObject gameObject, out WatchedPlayer watch )
|
||||
{
|
||||
watch = default;
|
||||
var name = gameObject.Name.ToString();
|
||||
return name.Length != 0 && Equip.TryGetValue( name, out watch );
|
||||
}
|
||||
|
||||
private static bool InvalidObjectKind( ObjectKind kind )
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
ObjectKind.BattleNpc => false,
|
||||
ObjectKind.EventNpc => false,
|
||||
ObjectKind.Player => false,
|
||||
ObjectKind.Retainer => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private GameObject? GetNextObject()
|
||||
{
|
||||
if( _frameTicker == GPosePlayerIdx - 1 )
|
||||
{
|
||||
_frameTicker = GPoseTableEnd;
|
||||
}
|
||||
else if( _frameTicker == _objects.Length - 1 )
|
||||
{
|
||||
_frameTicker = 0;
|
||||
foreach( var (_, equip) in Equip.Values.SelectMany( d => d.FoundActors.Where( p => !SeenActors.Contains( p.Key ) ) ) )
|
||||
{
|
||||
equip.Clear();
|
||||
}
|
||||
|
||||
SeenActors.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
++_frameTicker;
|
||||
}
|
||||
|
||||
return _objects[ _frameTicker ];
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate( object framework )
|
||||
{
|
||||
var newInGPose = _objects[ GPosePlayerIdx ] != null;
|
||||
|
||||
if( newInGPose != _inGPose )
|
||||
{
|
||||
if( newInGPose )
|
||||
{
|
||||
TriggerGPose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
_inGPose = newInGPose;
|
||||
}
|
||||
|
||||
for( var i = 0; i < ObjectsPerFrame; ++i )
|
||||
{
|
||||
var actor = GetNextObject();
|
||||
if( actor == null
|
||||
|| InvalidObjectKind( actor.ObjectKind )
|
||||
|| !TryGetPlayer( actor, out var watch ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var character = CheckGPoseObject( actor );
|
||||
if( _cancel )
|
||||
{
|
||||
_cancel = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if( character == null || character.ModelType() != 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = GetId( character );
|
||||
SeenActors.Add( id );
|
||||
|
||||
#if DEBUG
|
||||
PluginLog.Verbose( "Comparing Gear for {PlayerName:l} ({Id}) at 0x{Address:X}...", character.Name, id, character.Address.ToInt64() );
|
||||
#endif
|
||||
|
||||
if( !watch.FoundActors.TryGetValue( id, out var equip ) )
|
||||
{
|
||||
equip = new CharacterEquipment( character );
|
||||
watch.FoundActors[ id ] = equip;
|
||||
TriggerEvents( watch.RegisteredWatchers, character );
|
||||
}
|
||||
else if( !equip.CompareAndUpdate( character ) )
|
||||
{
|
||||
TriggerEvents( watch.RegisteredWatchers, character );
|
||||
}
|
||||
|
||||
break; // Only one comparison per frame.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Penumbra.PlayerWatch;
|
||||
|
||||
public class PlayerWatcher : IPlayerWatcher
|
||||
{
|
||||
public int Version
|
||||
=> 3;
|
||||
|
||||
private static PlayerWatchBase? _playerWatch;
|
||||
|
||||
public event PlayerChange? PlayerChanged;
|
||||
|
||||
public bool Active { get; set; } = true;
|
||||
|
||||
public bool Valid
|
||||
=> _playerWatch != null;
|
||||
|
||||
internal PlayerWatcher( Framework framework, ClientState clientState, ObjectTable objects )
|
||||
{
|
||||
_playerWatch ??= new PlayerWatchBase( framework, clientState, objects );
|
||||
_playerWatch.RegisterWatcher( this );
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
=> SetStatus( true );
|
||||
|
||||
public void Disable()
|
||||
=> SetStatus( false );
|
||||
|
||||
public void SetStatus( bool enabled )
|
||||
{
|
||||
Active = enabled && Valid;
|
||||
_playerWatch?.CheckActiveStatus();
|
||||
}
|
||||
|
||||
internal void Trigger( Character actor )
|
||||
=> PlayerChanged?.Invoke( actor );
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if( _playerWatch == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Active = false;
|
||||
PlayerChanged = null;
|
||||
_playerWatch.UnregisterWatcher( this );
|
||||
if( _playerWatch.RegisteredWatchers.Count == 0 )
|
||||
{
|
||||
_playerWatch.Dispose();
|
||||
_playerWatch = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckValidity()
|
||||
{
|
||||
if( !Valid )
|
||||
{
|
||||
throw new Exception( $"PlayerWatch was already disposed." );
|
||||
}
|
||||
}
|
||||
|
||||
public void AddPlayerToWatch( string name )
|
||||
{
|
||||
CheckValidity();
|
||||
_playerWatch!.AddPlayerToWatch( name, this );
|
||||
}
|
||||
|
||||
public void RemovePlayerFromWatch( string playerName )
|
||||
{
|
||||
CheckValidity();
|
||||
_playerWatch!.RemovePlayerFromWatch( playerName, this );
|
||||
}
|
||||
|
||||
public CharacterEquipment UpdatePlayerWithoutEvent( Character actor )
|
||||
{
|
||||
CheckValidity();
|
||||
return _playerWatch!.UpdatePlayerWithoutEvent( actor );
|
||||
}
|
||||
|
||||
public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers()
|
||||
{
|
||||
CheckValidity();
|
||||
return _playerWatch!.Equip
|
||||
.Where( kvp => kvp.Value.RegisteredWatchers.Contains( this ) )
|
||||
.Select( kvp => ( kvp.Key, kvp.Value.FoundActors.Select( kvp2 => ( kvp2.Key, kvp2.Value ) ).ToArray() ) );
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerWatchFactory
|
||||
{
|
||||
public static IPlayerWatcher Create( Framework framework, ClientState clientState, ObjectTable objects )
|
||||
=> new PlayerWatcher( framework, clientState, objects );
|
||||
}
|
||||
1
Penumbra.String
Submodule
1
Penumbra.String
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592
|
||||
91
Penumbra.sln
91
Penumbra.sln
|
|
@ -8,40 +8,91 @@ EndProject
|
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.github\workflows\build.yml = .github\workflows\build.yml
|
||||
Penumbra\Penumbra.json = Penumbra\Penumbra.json
|
||||
.github\workflows\release.yml = .github\workflows\release.yml
|
||||
repo.json = repo.json
|
||||
.github\workflows\test_release.yml = .github\workflows\test_release.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{01685BD8-8847-4B49-BF90-1683B4C76B0E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{87750518-1A20-40B4-9FC1-22F906EFB290}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
schemas\default_mod.json = schemas\default_mod.json
|
||||
schemas\group.json = schemas\group.json
|
||||
schemas\local_mod_data-v3.json = schemas\local_mod_data-v3.json
|
||||
schemas\mod_meta-v3.json = schemas\mod_meta-v3.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
schemas\structs\container.json = schemas\structs\container.json
|
||||
schemas\structs\group_combining.json = schemas\structs\group_combining.json
|
||||
schemas\structs\group_imc.json = schemas\structs\group_imc.json
|
||||
schemas\structs\group_multi.json = schemas\structs\group_multi.json
|
||||
schemas\structs\group_single.json = schemas\structs\group_single.json
|
||||
schemas\structs\manipulation.json = schemas\structs\manipulation.json
|
||||
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
|
||||
schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
|
||||
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
|
||||
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
|
||||
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
|
||||
schemas\structs\meta_est.json = schemas\structs\meta_est.json
|
||||
schemas\structs\meta_geqp.json = schemas\structs\meta_geqp.json
|
||||
schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
|
||||
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
|
||||
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
|
||||
schemas\structs\meta_shp.json = schemas\structs\meta_shp.json
|
||||
schemas\structs\option.json = schemas\structs\option.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{BFEA7504-1210-4F79-A7FE-BF03B6567E33} = {F89C9EAE-25C8-43BE-8108-5921E5A93502}
|
||||
{B03F276A-0572-4F62-AF86-EF62F6B80463} = {BFEA7504-1210-4F79-A7FE-BF03B6567E33}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
|
||||
EndGlobalSection
|
||||
|
|
|
|||
78
Penumbra/Api/Api/ApiHelpers.cs
Normal file
78
Penumbra/Api/Api/ApiHelpers.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class ApiHelpers(
|
||||
CollectionManager collectionManager,
|
||||
ObjectManager objects,
|
||||
CollectionResolver collectionResolver,
|
||||
ActorManager actors) : IApiService
|
||||
{
|
||||
/// <summary> Return the associated identifier for an object given by its index. </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal ActorIdentifier AssociatedIdentifier(int gameObjectIdx)
|
||||
{
|
||||
if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
|
||||
return ActorIdentifier.Invalid;
|
||||
|
||||
var ptr = objects[gameObjectIdx];
|
||||
return actors.FromObject(ptr, out _, false, true, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the collection associated to a current game object. If it does not exist, return the default collection.
|
||||
/// If the index is invalid, returns false and the default collection.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection)
|
||||
{
|
||||
collection = collectionManager.Active.Default;
|
||||
if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
|
||||
return false;
|
||||
|
||||
var ptr = objects[gameObjectIdx];
|
||||
var data = collectionResolver.IdentifyCollection(ptr.AsObject, false);
|
||||
if (data.Valid)
|
||||
collection = data.ModCollection;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown")
|
||||
{
|
||||
if (ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged)
|
||||
Penumbra.Log.Verbose($"[{name}] Called with {args}, returned {ec}.");
|
||||
else
|
||||
Penumbra.Log.Debug($"[{name}] Called with {args}, returned {ec}.");
|
||||
return ec;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal static LazyString Args(params object[] arguments)
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
return new LazyString(() => "no arguments");
|
||||
|
||||
return new LazyString(() =>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (var i = 0; i < arguments.Length / 2; ++i)
|
||||
{
|
||||
sb.Append(arguments[2 * i]);
|
||||
sb.Append(" = ");
|
||||
sb.Append(arguments[2 * i + 1]);
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
return sb.ToString(0, sb.Length - 2);
|
||||
});
|
||||
}
|
||||
}
|
||||
177
Penumbra/Api/Api/CollectionApi.cs
Normal file
177
Penumbra/Api/Api/CollectionApi.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService
|
||||
{
|
||||
public Dictionary<Guid, string> GetCollections()
|
||||
=> collections.Storage.ToDictionary(c => c.Identity.Id, c => c.Identity.Name);
|
||||
|
||||
public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier)
|
||||
{
|
||||
if (identifier.Length == 0)
|
||||
return [];
|
||||
|
||||
var list = new List<(Guid Id, string Name)>(4);
|
||||
if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty)
|
||||
list.Add((collection.Identity.Id, collection.Identity.Name));
|
||||
else if (identifier.Length >= 8)
|
||||
list.AddRange(collections.Storage.Where(c => c.Identity.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(c => (c.Identity.Id, c.Identity.Name)));
|
||||
|
||||
list.AddRange(collections.Storage
|
||||
.Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase)
|
||||
&& !list.Contains((c.Identity.Id, c.Identity.Name)))
|
||||
.Select(c => (c.Identity.Id, c.Identity.Name)));
|
||||
return list;
|
||||
}
|
||||
|
||||
public Func<string, (string ModDirectory, string ModName)[]> CheckCurrentChangedItemFunc()
|
||||
{
|
||||
var weakRef = new WeakReference<CollectionManager>(collections);
|
||||
return s =>
|
||||
{
|
||||
if (!weakRef.TryGetTarget(out var c))
|
||||
throw new ObjectDisposedException("The underlying collection storage of this IPC container was disposed.");
|
||||
|
||||
if (!c.Active.Current.ChangedItems.TryGetValue(s, out var d))
|
||||
return [];
|
||||
|
||||
return d.Item1.Select(m => (m is Mod mod ? mod.Identifier : string.Empty, m.Name.Text)).ToArray();
|
||||
};
|
||||
}
|
||||
|
||||
public Dictionary<string, object?> GetChangedItemsForCollection(Guid collectionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!collections.Storage.ById(collectionId, out var collection))
|
||||
collection = ModCollection.Empty;
|
||||
|
||||
if (collection.HasCache)
|
||||
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject());
|
||||
|
||||
Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
|
||||
return [];
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not obtain Changed Items for {collectionId}:\n{e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public (Guid Id, string Name)? GetCollection(ApiCollectionType type)
|
||||
{
|
||||
if (!Enum.IsDefined(type))
|
||||
return null;
|
||||
|
||||
var collection = collections.Active.ByType((CollectionType)type);
|
||||
return collection == null ? null : (collection.Identity.Id, collection.Identity.Name);
|
||||
}
|
||||
|
||||
internal (Guid Id, string Name)? GetCollection(byte type)
|
||||
=> GetCollection((ApiCollectionType)type);
|
||||
|
||||
public (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) GetCollectionForObject(int gameObjectIdx)
|
||||
{
|
||||
var id = helpers.AssociatedIdentifier(gameObjectIdx);
|
||||
if (!id.IsValid)
|
||||
return (false, false, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
|
||||
|
||||
if (collections.Active.Individuals.TryGetValue(id, out var collection))
|
||||
return (true, true, (collection.Identity.Id, collection.Identity.Name));
|
||||
|
||||
helpers.AssociatedCollection(gameObjectIdx, out collection);
|
||||
return (true, false, (collection.Identity.Id, collection.Identity.Name));
|
||||
}
|
||||
|
||||
public Guid[] GetCollectionByName(string name)
|
||||
=> collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id)
|
||||
.ToArray();
|
||||
|
||||
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId,
|
||||
bool allowCreateNew, bool allowDelete)
|
||||
{
|
||||
if (!Enum.IsDefined(type))
|
||||
return (PenumbraApiEc.InvalidArgument, null);
|
||||
|
||||
var oldCollection = collections.Active.ByType((CollectionType)type);
|
||||
var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
|
||||
if (collectionId == null)
|
||||
{
|
||||
if (old == null)
|
||||
return (PenumbraApiEc.NothingChanged, old);
|
||||
|
||||
if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface)
|
||||
return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
|
||||
|
||||
collections.Active.RemoveSpecialCollection((CollectionType)type);
|
||||
return (PenumbraApiEc.Success, old);
|
||||
}
|
||||
|
||||
if (!collections.Storage.ById(collectionId.Value, out var collection))
|
||||
return (PenumbraApiEc.CollectionMissing, old);
|
||||
|
||||
if (old == null)
|
||||
{
|
||||
if (!allowCreateNew)
|
||||
return (PenumbraApiEc.AssignmentCreationDisallowed, old);
|
||||
|
||||
collections.Active.CreateSpecialCollection((CollectionType)type);
|
||||
}
|
||||
else if (old.Value.Item1 == collection.Identity.Id)
|
||||
{
|
||||
return (PenumbraApiEc.NothingChanged, old);
|
||||
}
|
||||
|
||||
collections.Active.SetCollection(collection, (CollectionType)type);
|
||||
return (PenumbraApiEc.Success, old);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollectionForObject(int gameObjectIdx, Guid? collectionId,
|
||||
bool allowCreateNew, bool allowDelete)
|
||||
{
|
||||
var id = helpers.AssociatedIdentifier(gameObjectIdx);
|
||||
if (!id.IsValid)
|
||||
return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
|
||||
|
||||
var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null;
|
||||
var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
|
||||
if (collectionId == null)
|
||||
{
|
||||
if (old == null)
|
||||
return (PenumbraApiEc.NothingChanged, old);
|
||||
|
||||
if (!allowDelete)
|
||||
return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
|
||||
|
||||
var idx = collections.Active.Individuals.Index(id);
|
||||
collections.Active.RemoveIndividualCollection(idx);
|
||||
return (PenumbraApiEc.Success, old);
|
||||
}
|
||||
|
||||
if (!collections.Storage.ById(collectionId.Value, out var collection))
|
||||
return (PenumbraApiEc.CollectionMissing, old);
|
||||
|
||||
if (old == null)
|
||||
{
|
||||
if (!allowCreateNew)
|
||||
return (PenumbraApiEc.AssignmentCreationDisallowed, old);
|
||||
|
||||
var ids = collections.Active.Individuals.GetGroup(id);
|
||||
collections.Active.CreateIndividualCollection(ids);
|
||||
}
|
||||
else if (old.Value.Item1 == collection.Identity.Id)
|
||||
{
|
||||
return (PenumbraApiEc.NothingChanged, old);
|
||||
}
|
||||
|
||||
collections.Active.SetCollection(collection, CollectionType.Individual, collections.Active.Individuals.Index(id));
|
||||
return (PenumbraApiEc.Success, old);
|
||||
}
|
||||
}
|
||||
54
Penumbra/Api/Api/EditingApi.cs
Normal file
54
Penumbra/Api/Api/EditingApi.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Import.Textures;
|
||||
using TextureType = Penumbra.Api.Enums.TextureType;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IApiService
|
||||
{
|
||||
public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps)
|
||||
=> textureType switch
|
||||
{
|
||||
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
|
||||
TextureType.Targa => textureManager.SaveTga(inputFile, outputFile),
|
||||
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile),
|
||||
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile),
|
||||
TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile),
|
||||
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile),
|
||||
TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, inputFile, outputFile),
|
||||
TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, inputFile, outputFile),
|
||||
TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, inputFile, outputFile),
|
||||
TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, inputFile, outputFile),
|
||||
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
|
||||
};
|
||||
|
||||
// @formatter:off
|
||||
public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps)
|
||||
=> textureType switch
|
||||
{
|
||||
TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Targa => textureManager.SaveTga(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
|
||||
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
|
||||
};
|
||||
// @formatter:on
|
||||
}
|
||||
123
Penumbra/Api/Api/GameStateApi.cs
Normal file
123
Penumbra/Api/Api/GameStateApi.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly DrawObjectState _drawObjectState;
|
||||
private readonly CutsceneService _cutsceneService;
|
||||
private readonly ResourceLoader _resourceLoader;
|
||||
|
||||
public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
|
||||
ResourceLoader resourceLoader, DrawObjectState drawObjectState)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_collectionResolver = collectionResolver;
|
||||
_cutsceneService = cutsceneService;
|
||||
_resourceLoader = resourceLoader;
|
||||
_drawObjectState = drawObjectState;
|
||||
_resourceLoader.ResourceLoaded += OnResourceLoaded;
|
||||
_resourceLoader.PapRequested += OnPapRequested;
|
||||
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
|
||||
}
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
|
||||
_resourceLoader.PapRequested -= OnPapRequested;
|
||||
_communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
|
||||
}
|
||||
|
||||
public event CreatedCharacterBaseDelegate? CreatedCharacterBase;
|
||||
public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved;
|
||||
|
||||
public event CreatingCharacterBaseDelegate? CreatingCharacterBase
|
||||
{
|
||||
add
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
_communicator.CreatingCharacterBase.Subscribe(new Action<nint, Guid, nint, nint, nint>(value),
|
||||
Communication.CreatingCharacterBase.Priority.Api);
|
||||
}
|
||||
remove
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
_communicator.CreatingCharacterBase.Unsubscribe(new Action<nint, Guid, nint, nint, nint>(value));
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject)
|
||||
{
|
||||
var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
||||
return (data.AssociatedGameObject, (Id: data.ModCollection.Identity.Id, Name: data.ModCollection.Identity.Name));
|
||||
}
|
||||
|
||||
public int GetCutsceneParentIndex(int actorIdx)
|
||||
=> _cutsceneService.GetParentIndex(actorIdx);
|
||||
|
||||
public Func<int, int> GetCutsceneParentIndexFunc()
|
||||
{
|
||||
var weakRef = new WeakReference<CutsceneService>(_cutsceneService);
|
||||
return idx =>
|
||||
{
|
||||
if (!weakRef.TryGetTarget(out var c))
|
||||
throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed.");
|
||||
|
||||
return c.GetParentIndex(idx);
|
||||
};
|
||||
}
|
||||
|
||||
public Func<nint, nint> GetGameObjectFromDrawObjectFunc()
|
||||
{
|
||||
var weakRef = new WeakReference<DrawObjectState>(_drawObjectState);
|
||||
return model =>
|
||||
{
|
||||
if (!weakRef.TryGetTarget(out var c))
|
||||
throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed.");
|
||||
|
||||
return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero;
|
||||
};
|
||||
}
|
||||
|
||||
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
|
||||
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.InvalidArgument;
|
||||
|
||||
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
|
||||
{
|
||||
if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
|
||||
{
|
||||
var original = originalPath.ToString();
|
||||
GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
|
||||
manipulatedPath?.ToString() ?? original);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPapRequested(Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
|
||||
{
|
||||
if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
|
||||
{
|
||||
var original = originalPath.ToString();
|
||||
GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
|
||||
manipulatedPath?.ToString() ?? original);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject)
|
||||
=> CreatedCharacterBase?.Invoke(gameObject, collection.Identity.Id, drawObject);
|
||||
}
|
||||
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Penumbra.Api.Api;
|
||||
|
||||
public static class IdentityChecker
|
||||
{
|
||||
public static bool Check(string identity)
|
||||
=> true;
|
||||
}
|
||||
544
Penumbra/Api/Api/MetaApi.cs
Normal file
544
Penumbra/Api/Api/MetaApi.cs
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Files.AtchStructs;
|
||||
using Penumbra.GameData.Files.Utility;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers)
|
||||
: IPenumbraApiMeta, IApiService
|
||||
{
|
||||
public string GetPlayerMetaManipulations()
|
||||
{
|
||||
var collection = collectionResolver.PlayerCollection();
|
||||
return CompressMetaManipulations(collection);
|
||||
}
|
||||
|
||||
public string GetMetaManipulations(int gameObjectIdx)
|
||||
{
|
||||
helpers.AssociatedCollection(gameObjectIdx, out var collection);
|
||||
return CompressMetaManipulations(collection);
|
||||
}
|
||||
|
||||
public Task<string> GetPlayerMetaManipulationsAsync()
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
|
||||
return CompressMetaManipulations(playerCollection);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<string> GetMetaManipulationsAsync(int gameObjectIdx)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var playerCollection = await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
helpers.AssociatedCollection(gameObjectIdx, out var collection);
|
||||
return collection;
|
||||
}).ConfigureAwait(false);
|
||||
return CompressMetaManipulations(playerCollection);
|
||||
});
|
||||
}
|
||||
|
||||
internal static string CompressMetaManipulations(ModCollection collection)
|
||||
=> CompressMetaManipulationsV1(collection);
|
||||
|
||||
private static string CompressMetaManipulationsV0(ModCollection collection)
|
||||
{
|
||||
var array = new JArray();
|
||||
if (collection.MetaCache is { } cache)
|
||||
{
|
||||
MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key));
|
||||
MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair<ImcIdentifier, ImcEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair<EqpIdentifier, EqpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair<EqdpIdentifier, EqdpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair<ShpIdentifier, ShpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair<AtrIdentifier, AtrEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
}
|
||||
|
||||
return Functions.ToCompressedBase64(array, 0);
|
||||
}
|
||||
|
||||
private static unsafe string CompressMetaManipulationsV1(ModCollection? collection)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
ms.Capacity = 1024;
|
||||
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
|
||||
{
|
||||
zipStream.Write((byte)1);
|
||||
zipStream.Write("META0001"u8);
|
||||
if (collection?.MetaCache is not { } cache)
|
||||
{
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
zipStream.Write(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteCache(zipStream, cache.Imc);
|
||||
WriteCache(zipStream, cache.Eqp);
|
||||
WriteCache(zipStream, cache.Eqdp);
|
||||
WriteCache(zipStream, cache.Est);
|
||||
WriteCache(zipStream, cache.Rsp);
|
||||
WriteCache(zipStream, cache.Gmp);
|
||||
cache.GlobalEqp.EnterReadLock();
|
||||
|
||||
try
|
||||
{
|
||||
zipStream.Write(cache.GlobalEqp.Count);
|
||||
foreach (var (globalEqp, _) in cache.GlobalEqp)
|
||||
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache.GlobalEqp.ExitReadLock();
|
||||
}
|
||||
|
||||
WriteCache(zipStream, cache.Atch);
|
||||
WriteCache(zipStream, cache.Shp);
|
||||
WriteCache(zipStream, cache.Atr);
|
||||
}
|
||||
}
|
||||
|
||||
ms.Flush();
|
||||
ms.Position = 0;
|
||||
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
|
||||
return Convert.ToBase64String(data);
|
||||
|
||||
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache)
|
||||
where TKey : unmanaged, IMetaIdentifier
|
||||
where TValue : unmanaged
|
||||
{
|
||||
metaCache.EnterReadLock();
|
||||
try
|
||||
{
|
||||
stream.Write(metaCache.Count);
|
||||
foreach (var (identifier, (_, value)) in metaCache)
|
||||
{
|
||||
stream.Write(identifier);
|
||||
stream.Write(value);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
metaCache.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8);
|
||||
public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8);
|
||||
public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P';
|
||||
public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8);
|
||||
public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8);
|
||||
public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8);
|
||||
public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P';
|
||||
public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H';
|
||||
public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8);
|
||||
public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8);
|
||||
|
||||
private static unsafe string CompressMetaManipulationsV2(ModCollection? collection)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
ms.Capacity = 1024;
|
||||
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
|
||||
{
|
||||
zipStream.Write((byte)2);
|
||||
zipStream.Write("META0002"u8);
|
||||
if (collection?.MetaCache is { } cache)
|
||||
{
|
||||
WriteCache(zipStream, cache.Imc, ImcKey);
|
||||
WriteCache(zipStream, cache.Eqp, EqpKey);
|
||||
WriteCache(zipStream, cache.Eqdp, EqdpKey);
|
||||
WriteCache(zipStream, cache.Est, EstKey);
|
||||
WriteCache(zipStream, cache.Rsp, RspKey);
|
||||
WriteCache(zipStream, cache.Gmp, GmpKey);
|
||||
cache.GlobalEqp.EnterReadLock();
|
||||
|
||||
try
|
||||
{
|
||||
if (cache.GlobalEqp.Count > 0)
|
||||
{
|
||||
zipStream.Write(GeqpKey);
|
||||
zipStream.Write(cache.GlobalEqp.Count);
|
||||
foreach (var (globalEqp, _) in cache.GlobalEqp)
|
||||
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache.GlobalEqp.ExitReadLock();
|
||||
}
|
||||
|
||||
WriteCache(zipStream, cache.Atch, AtchKey);
|
||||
WriteCache(zipStream, cache.Shp, ShpKey);
|
||||
WriteCache(zipStream, cache.Atr, AtrKey);
|
||||
}
|
||||
}
|
||||
|
||||
ms.Flush();
|
||||
ms.Position = 0;
|
||||
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
|
||||
return Convert.ToBase64String(data);
|
||||
|
||||
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache, uint label)
|
||||
where TKey : unmanaged, IMetaIdentifier
|
||||
where TValue : unmanaged
|
||||
{
|
||||
metaCache.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (metaCache.Count <= 0)
|
||||
return;
|
||||
|
||||
stream.Write(label);
|
||||
stream.Write(metaCache.Count);
|
||||
foreach (var (identifier, (_, value)) in metaCache)
|
||||
{
|
||||
stream.Write(identifier);
|
||||
stream.Write(value);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
metaCache.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert manipulations from a transmitted base64 string to actual manipulations.
|
||||
/// The empty string is treated as an empty set.
|
||||
/// Only returns true if all conversions are successful and distinct.
|
||||
/// </summary>
|
||||
internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips, out byte version)
|
||||
{
|
||||
if (manipString.Length == 0)
|
||||
{
|
||||
manips = new MetaDictionary();
|
||||
version = byte.MaxValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(manipString);
|
||||
using var compressedStream = new MemoryStream(bytes);
|
||||
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
|
||||
using var resultStream = new MemoryStream();
|
||||
zipStream.CopyTo(resultStream);
|
||||
resultStream.Flush();
|
||||
resultStream.Position = 0;
|
||||
var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length);
|
||||
version = data[0];
|
||||
data = data[1..];
|
||||
switch (version)
|
||||
{
|
||||
case 0: return ConvertManipsV0(data, out manips);
|
||||
case 1: return ConvertManipsV1(data, out manips);
|
||||
case 2: return ConvertManipsV2(data, out manips);
|
||||
default:
|
||||
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}");
|
||||
manips = null;
|
||||
version = byte.MaxValue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV2(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (!data.StartsWith("META0002"u8))
|
||||
{
|
||||
Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix.");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
manips = new MetaDictionary();
|
||||
var r = new SpanBinaryReader(data[8..]);
|
||||
while (r.Remaining > 4)
|
||||
{
|
||||
var prefix = r.ReadUInt32();
|
||||
var count = r.Remaining > 4 ? r.ReadInt32() : 0;
|
||||
if (count is 0)
|
||||
continue;
|
||||
|
||||
switch (prefix)
|
||||
{
|
||||
case ImcKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<ImcIdentifier>();
|
||||
var value = r.Read<ImcEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EqpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqpIdentifier>();
|
||||
var value = r.Read<EqpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EqdpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqdpIdentifier>();
|
||||
var value = r.Read<EqdpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EstKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EstIdentifier>();
|
||||
var value = r.Read<EstEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case RspKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<RspIdentifier>();
|
||||
var value = r.Read<RspEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case GmpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<GmpIdentifier>();
|
||||
var value = r.Read<GmpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case GeqpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<GlobalEqpManipulation>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case AtchKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtchIdentifier>();
|
||||
var value = r.Read<AtchEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case ShpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<ShpIdentifier>();
|
||||
var value = r.Read<ShpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case AtrKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtrIdentifier>();
|
||||
var value = r.Read<AtrEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (!data.StartsWith("META0001"u8))
|
||||
{
|
||||
Penumbra.Log.Debug($"Invalid manipulations of version 1, does not start with valid prefix.");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
manips = new MetaDictionary();
|
||||
var r = new SpanBinaryReader(data[8..]);
|
||||
var imcCount = r.ReadInt32();
|
||||
for (var i = 0; i < imcCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<ImcIdentifier>();
|
||||
var value = r.Read<ImcEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var eqpCount = r.ReadInt32();
|
||||
for (var i = 0; i < eqpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqpIdentifier>();
|
||||
var value = r.Read<EqpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var eqdpCount = r.ReadInt32();
|
||||
for (var i = 0; i < eqdpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqdpIdentifier>();
|
||||
var value = r.Read<EqdpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var estCount = r.ReadInt32();
|
||||
for (var i = 0; i < estCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<EstIdentifier>();
|
||||
var value = r.Read<EstEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var rspCount = r.ReadInt32();
|
||||
for (var i = 0; i < rspCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<RspIdentifier>();
|
||||
var value = r.Read<RspEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var gmpCount = r.ReadInt32();
|
||||
for (var i = 0; i < gmpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<GmpIdentifier>();
|
||||
var value = r.Read<GmpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var globalEqpCount = r.ReadInt32();
|
||||
for (var i = 0; i < globalEqpCount; ++i)
|
||||
{
|
||||
var manip = r.Read<GlobalEqpManipulation>();
|
||||
if (!manip.Validate() || !manips.TryAdd(manip))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Atch was added after there were already some V1 around, so check for size here.
|
||||
if (r.Position < r.Count)
|
||||
{
|
||||
var atchCount = r.ReadInt32();
|
||||
for (var i = 0; i < atchCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtchIdentifier>();
|
||||
var value = r.Read<AtchEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shp and Atr was added later
|
||||
if (r.Position < r.Count)
|
||||
{
|
||||
var shpCount = r.ReadInt32();
|
||||
for (var i = 0; i < shpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<ShpIdentifier>();
|
||||
var value = r.Read<ShpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var atrCount = r.ReadInt32();
|
||||
for (var i = 0; i < atrCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtrIdentifier>();
|
||||
var value = r.Read<AtrEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV0(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(data);
|
||||
manips = JsonConvert.DeserializeObject<MetaDictionary>(json);
|
||||
return manips != null;
|
||||
}
|
||||
|
||||
internal void TestMetaManipulations()
|
||||
{
|
||||
var collection = collectionResolver.PlayerCollection();
|
||||
var dict = new MetaDictionary(collection.MetaCache);
|
||||
var count = dict.Count;
|
||||
|
||||
var watch = Stopwatch.StartNew();
|
||||
var v0 = CompressMetaManipulationsV0(collection);
|
||||
var v0Time = watch.ElapsedMilliseconds;
|
||||
|
||||
watch.Restart();
|
||||
var v1 = CompressMetaManipulationsV1(collection);
|
||||
var v1Time = watch.ElapsedMilliseconds;
|
||||
|
||||
watch.Restart();
|
||||
var v1Success = ConvertManips(v1, out var v1Roundtrip, out _);
|
||||
var v1RoundtripTime = watch.ElapsedMilliseconds;
|
||||
|
||||
watch.Restart();
|
||||
var v0Success = ConvertManips(v0, out var v0Roundtrip, out _);
|
||||
var v0RoundtripTime = watch.ElapsedMilliseconds;
|
||||
|
||||
Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal");
|
||||
Penumbra.Log.Information(
|
||||
$"0 | {count} | {v0Time} | {v0.Length} | {v0Success} | {v0Roundtrip?.Count} | {v0RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
|
||||
Penumbra.Log.Information(
|
||||
$"1 | {count} | {v1Time} | {v1.Length} | {v1Success} | {v1Roundtrip?.Count} | {v1RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
|
||||
}
|
||||
}
|
||||
362
Penumbra/Api/Api/ModSettingsApi.cs
Normal file
362
Penumbra/Api/Api/ModSettingsApi.cs
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Manager.OptionEditor;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.Mods.SubMods;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
|
||||
{
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly CollectionEditor _collectionEditor;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public ModSettingsApi(CollectionResolver collectionResolver,
|
||||
ModManager modManager,
|
||||
CollectionManager collectionManager,
|
||||
CollectionEditor collectionEditor,
|
||||
CommunicatorService communicator)
|
||||
{
|
||||
_collectionResolver = collectionResolver;
|
||||
_modManager = modManager;
|
||||
_collectionManager = collectionManager;
|
||||
_collectionEditor = collectionEditor;
|
||||
_communicator = communicator;
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings);
|
||||
_communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api);
|
||||
_communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api);
|
||||
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
|
||||
_communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited);
|
||||
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
|
||||
}
|
||||
|
||||
public event ModSettingChangedDelegate? ModSettingChanged;
|
||||
|
||||
public AvailableModSettings? GetAvailableModSettings(string modDirectory, string modName)
|
||||
{
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return null;
|
||||
|
||||
var dict = new Dictionary<string, (string[], int)>(mod.Groups.Count);
|
||||
foreach (var g in mod.Groups)
|
||||
dict.Add(g.Name, (g.Options.Select(o => o.Name).ToArray(), (int)g.Type));
|
||||
return new AvailableModSettings(dict);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory,
|
||||
string modName, bool ignoreInheritance)
|
||||
{
|
||||
var ret = GetCurrentModSettingsWithTemp(collectionId, modDirectory, modName, ignoreInheritance, true, 0);
|
||||
if (ret.Item2 is null)
|
||||
return (ret.Item1, null);
|
||||
|
||||
return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4));
|
||||
}
|
||||
|
||||
public PenumbraApiEc GetSettingsInAllCollections(string modDirectory, string modName,
|
||||
out Dictionary<Guid, (bool, int, Dictionary<string, List<string>>, bool, bool)> settings,
|
||||
bool ignoreTemporaryCollections = false)
|
||||
{
|
||||
settings = [];
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return PenumbraApiEc.ModMissing;
|
||||
|
||||
var collections = ignoreTemporaryCollections
|
||||
? _collectionManager.Storage.Where(c => c != ModCollection.Empty)
|
||||
: _collectionManager.Storage.Where(c => c != ModCollection.Empty).Concat(_collectionManager.Temp.Values);
|
||||
settings = [];
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
if (GetCurrentSettings(collection, mod, false, false, 0) is { } s)
|
||||
settings.Add(collection.Identity.Id, s);
|
||||
}
|
||||
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId,
|
||||
string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key)
|
||||
{
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return (PenumbraApiEc.ModMissing, null);
|
||||
|
||||
if (!_collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return (PenumbraApiEc.CollectionMissing, null);
|
||||
|
||||
if (collection.Identity.Id == Guid.Empty)
|
||||
return (PenumbraApiEc.Success, null);
|
||||
|
||||
if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings)
|
||||
return (PenumbraApiEc.Success, settings);
|
||||
|
||||
return (PenumbraApiEc.Success, null);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, Dictionary<string, (bool, int, Dictionary<string, List<string>>, bool, bool)>?) GetAllModSettings(Guid collectionId,
|
||||
bool ignoreInheritance, bool ignoreTemporary, int key)
|
||||
{
|
||||
if (!_collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return (PenumbraApiEc.CollectionMissing, null);
|
||||
|
||||
if (collection.Identity.Id == Guid.Empty)
|
||||
return (PenumbraApiEc.Success, []);
|
||||
|
||||
var ret = new Dictionary<string, (bool, int, Dictionary<string, List<string>>, bool, bool)>(_modManager.Count);
|
||||
foreach (var mod in _modManager)
|
||||
{
|
||||
if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings)
|
||||
ret[mod.Identifier] = settings;
|
||||
}
|
||||
|
||||
return (PenumbraApiEc.Success, ret);
|
||||
}
|
||||
|
||||
public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit",
|
||||
inherit.ToString());
|
||||
|
||||
if (collectionId == Guid.Empty)
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
if (!_collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
|
||||
|
||||
var ret = _collectionEditor.SetModInheritance(collection, mod, inherit)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.NothingChanged;
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc TrySetMod(Guid collectionId, string modDirectory, string modName, bool enabled)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled);
|
||||
if (!_collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
|
||||
|
||||
var ret = _collectionEditor.SetModState(collection, mod, enabled)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.NothingChanged;
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc TrySetModPriority(Guid collectionId, string modDirectory, string modName, int priority)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Priority", priority);
|
||||
|
||||
if (!_collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
|
||||
|
||||
var ret = _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority))
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.NothingChanged;
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc TrySetModSetting(Guid collectionId, string modDirectory, string modName, string optionGroupName, string optionName)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
|
||||
optionGroupName, "OptionName", optionName);
|
||||
|
||||
if (!_collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
|
||||
|
||||
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
|
||||
if (groupIdx < 0)
|
||||
return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
|
||||
|
||||
var optionIdx = mod.Groups[groupIdx].Options.IndexOf(o => o.Name == optionName);
|
||||
if (optionIdx < 0)
|
||||
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
|
||||
|
||||
var setting = mod.Groups[groupIdx].Behaviour switch
|
||||
{
|
||||
GroupDrawBehaviour.MultiSelection => Setting.Multi(optionIdx),
|
||||
GroupDrawBehaviour.SingleSelection => Setting.Single(optionIdx),
|
||||
_ => Setting.Zero,
|
||||
};
|
||||
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.NothingChanged;
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc TrySetModSettings(Guid collectionId, string modDirectory, string modName, string optionGroupName,
|
||||
IReadOnlyList<string> optionNames)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
|
||||
optionGroupName, "#optionNames", optionNames.Count);
|
||||
|
||||
if (!_collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
|
||||
|
||||
var settingSuccess = ConvertModSetting(mod, optionGroupName, optionNames, out var groupIdx, out var setting);
|
||||
if (settingSuccess is not PenumbraApiEc.Success)
|
||||
return ApiHelpers.Return(settingSuccess, args);
|
||||
|
||||
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.NothingChanged;
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc CopyModSettings(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId.HasValue ? collectionId.Value.ToString() : "NULL",
|
||||
"From", modDirectoryFrom, "To", modDirectoryTo);
|
||||
var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase));
|
||||
var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase));
|
||||
if (collectionId == null)
|
||||
foreach (var collection in _collectionManager.Storage)
|
||||
_collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
|
||||
else if (_collectionManager.Storage.ById(collectionId.Value, out var collection))
|
||||
_collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
|
||||
else
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, args);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private (bool, int, Dictionary<string, List<string>>, bool, bool)? GetCurrentSettings(ModCollection collection, Mod mod,
|
||||
bool ignoreInheritance, bool ignoreTemporary, int key)
|
||||
{
|
||||
var settings = collection.Settings.Settings[mod.Index];
|
||||
if (!ignoreTemporary && settings.TempSettings is { } tempSettings && (tempSettings.Lock <= 0 || tempSettings.Lock == key))
|
||||
{
|
||||
if (!tempSettings.ForceInherit)
|
||||
return (tempSettings.Enabled, tempSettings.Priority.Value, tempSettings.ConvertToShareable(mod).Settings,
|
||||
false, true);
|
||||
if (!ignoreInheritance && collection.GetActualSettings(mod.Index).Settings is { } actualSettingsTemp)
|
||||
return (actualSettingsTemp.Enabled, actualSettingsTemp.Priority.Value,
|
||||
actualSettingsTemp.ConvertToShareable(mod).Settings, true, true);
|
||||
}
|
||||
|
||||
if (settings.Settings is { } ownSettings)
|
||||
return (ownSettings.Enabled, ownSettings.Priority.Value, ownSettings.ConvertToShareable(mod).Settings, false,
|
||||
false);
|
||||
if (!ignoreInheritance && collection.GetInheritedSettings(mod.Index).Settings is { } actualSettings)
|
||||
return (actualSettings.Enabled, actualSettings.Priority.Value,
|
||||
actualSettings.ConvertToShareable(mod).Settings, true, false);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private void TriggerSettingEdited(Mod mod)
|
||||
{
|
||||
var collection = _collectionResolver.PlayerCollection();
|
||||
var (settings, parent) = collection.GetActualSettings(mod.Index);
|
||||
if (settings is { Enabled: true })
|
||||
ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection);
|
||||
}
|
||||
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
|
||||
{
|
||||
if (type == ModPathChangeType.Reloaded)
|
||||
TriggerSettingEdited(mod);
|
||||
}
|
||||
|
||||
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited)
|
||||
=> ModSettingChanged?.Invoke(type, collection.Identity.Id, mod?.ModPath.Name ?? string.Empty, inherited);
|
||||
|
||||
private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
|
||||
int moveIndex)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModOptionChangeType.GroupDeleted:
|
||||
case ModOptionChangeType.GroupMoved:
|
||||
case ModOptionChangeType.GroupTypeChanged:
|
||||
case ModOptionChangeType.PriorityChanged:
|
||||
case ModOptionChangeType.OptionDeleted:
|
||||
case ModOptionChangeType.OptionMoved:
|
||||
case ModOptionChangeType.OptionFilesChanged:
|
||||
case ModOptionChangeType.OptionFilesAdded:
|
||||
case ModOptionChangeType.OptionSwapsChanged:
|
||||
case ModOptionChangeType.OptionMetaChanged:
|
||||
TriggerSettingEdited(mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModFileChanged(Mod mod, FileRegistry file)
|
||||
{
|
||||
if (file.CurrentUsage == 0)
|
||||
return;
|
||||
|
||||
TriggerSettingEdited(mod);
|
||||
}
|
||||
|
||||
public static PenumbraApiEc ConvertModSetting(Mod mod, string groupName, IReadOnlyList<string> optionNames, out int groupIndex,
|
||||
out Setting setting)
|
||||
{
|
||||
groupIndex = mod.Groups.IndexOf(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase));
|
||||
setting = Setting.Zero;
|
||||
if (groupIndex < 0)
|
||||
return PenumbraApiEc.OptionGroupMissing;
|
||||
|
||||
switch (mod.Groups[groupIndex])
|
||||
{
|
||||
case { Behaviour: GroupDrawBehaviour.SingleSelection } single:
|
||||
{
|
||||
var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]);
|
||||
if (optionIdx < 0)
|
||||
return PenumbraApiEc.OptionMissing;
|
||||
|
||||
setting = Setting.Single(optionIdx);
|
||||
break;
|
||||
}
|
||||
case { Behaviour: GroupDrawBehaviour.MultiSelection } multi:
|
||||
{
|
||||
foreach (var name in optionNames)
|
||||
{
|
||||
var optionIdx = multi.Options.IndexOf(o => o.Name == name);
|
||||
if (optionIdx < 0)
|
||||
return PenumbraApiEc.OptionMissing;
|
||||
|
||||
setting |= Setting.Multi(optionIdx);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
}
|
||||
165
Penumbra/Api/Api/ModsApi.cs
Normal file
165
Penumbra/Api/Api/ModsApi.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Compression;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly Configuration _config;
|
||||
private readonly ModFileSystem _modFileSystem;
|
||||
private readonly MigrationManager _migrationManager;
|
||||
|
||||
public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem,
|
||||
CommunicatorService communicator, MigrationManager migrationManager)
|
||||
{
|
||||
_modManager = modManager;
|
||||
_modImportManager = modImportManager;
|
||||
_config = config;
|
||||
_modFileSystem = modFileSystem;
|
||||
_communicator = communicator;
|
||||
_migrationManager = migrationManager;
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods);
|
||||
}
|
||||
|
||||
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break;
|
||||
case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break;
|
||||
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
|
||||
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetModList()
|
||||
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
|
||||
|
||||
public PenumbraApiEc InstallMod(string modFilePackagePath)
|
||||
{
|
||||
if (!File.Exists(modFilePackagePath))
|
||||
return ApiHelpers.Return(PenumbraApiEc.FileMissing, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
|
||||
|
||||
_modImportManager.AddUnpack(modFilePackagePath);
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
|
||||
}
|
||||
|
||||
public PenumbraApiEc ReloadMod(string modDirectory, string modName)
|
||||
{
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
|
||||
|
||||
_modManager.ReloadMod(mod);
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
|
||||
}
|
||||
|
||||
public PenumbraApiEc AddMod(string modDirectory)
|
||||
{
|
||||
var args = ApiHelpers.Args("ModDirectory", modDirectory);
|
||||
|
||||
var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory)));
|
||||
if (!dir.Exists)
|
||||
return ApiHelpers.Return(PenumbraApiEc.FileMissing, args);
|
||||
|
||||
if (dir.Parent == null
|
||||
|| Path.TrimEndingDirectorySeparator(Path.GetFullPath(_modManager.BasePath.FullName))
|
||||
!= Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
_modManager.AddMod(dir, true);
|
||||
if (_config.MigrateImportedModelsToV6)
|
||||
{
|
||||
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
|
||||
_migrationManager.Await();
|
||||
}
|
||||
|
||||
if (_config.UseFileSystemCompression)
|
||||
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
|
||||
CompressionAlgorithm.Xpress8K, false);
|
||||
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc DeleteMod(string modDirectory, string modName)
|
||||
{
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
|
||||
|
||||
_modManager.DeleteMod(mod);
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
|
||||
}
|
||||
|
||||
public event Action<string>? ModDeleted;
|
||||
public event Action<string>? ModAdded;
|
||||
public event Action<string, string>? ModMoved;
|
||||
|
||||
public event Action<JObject, ushort, string>? CreatingPcp
|
||||
{
|
||||
add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi);
|
||||
remove => _communicator.PcpCreation.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public event Action<JObject, string, Guid>? ParsingPcp
|
||||
{
|
||||
add => _communicator.PcpParsing.Subscribe(value!, PcpParsing.Priority.ModsApi);
|
||||
remove => _communicator.PcpParsing.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
|
||||
{
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|
||||
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
|
||||
|
||||
var fullPath = leaf.FullName();
|
||||
var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath);
|
||||
var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name);
|
||||
return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault);
|
||||
}
|
||||
|
||||
public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath)
|
||||
{
|
||||
if (newPath.Length == 0)
|
||||
return PenumbraApiEc.InvalidArgument;
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|
||||
return PenumbraApiEc.ModMissing;
|
||||
|
||||
try
|
||||
{
|
||||
_modFileSystem.RenameAndMove(leaf, newPath);
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return PenumbraApiEc.PathRenameFailed;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, object?> GetChangedItems(string modDirectory, string modName)
|
||||
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject())
|
||||
: [];
|
||||
|
||||
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>> GetChangedItemAdapterDictionary()
|
||||
=> new ModChangedItemAdapter(new WeakReference<ModStorage>(_modManager));
|
||||
|
||||
public IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)> GetChangedItemAdapterList()
|
||||
=> new ModChangedItemAdapter(new WeakReference<ModStorage>(_modManager));
|
||||
}
|
||||
43
Penumbra/Api/Api/PenumbraApi.cs
Normal file
43
Penumbra/Api/Api/PenumbraApi.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using OtterGui.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class PenumbraApi(
|
||||
CollectionApi collection,
|
||||
EditingApi editing,
|
||||
GameStateApi gameState,
|
||||
MetaApi meta,
|
||||
ModsApi mods,
|
||||
ModSettingsApi modSettings,
|
||||
PluginStateApi pluginState,
|
||||
RedrawApi redraw,
|
||||
ResolveApi resolve,
|
||||
ResourceTreeApi resourceTree,
|
||||
TemporaryApi temporary,
|
||||
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
||||
{
|
||||
public const int BreakingVersion = 5;
|
||||
public const int FeatureVersion = 13;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Valid = false;
|
||||
}
|
||||
|
||||
public (int Breaking, int Feature) ApiVersion
|
||||
=> (BreakingVersion, FeatureVersion);
|
||||
|
||||
public bool Valid { get; private set; } = true;
|
||||
public IPenumbraApiCollection Collection { get; } = collection;
|
||||
public IPenumbraApiEditing Editing { get; } = editing;
|
||||
public IPenumbraApiGameState GameState { get; } = gameState;
|
||||
public IPenumbraApiMeta Meta { get; } = meta;
|
||||
public IPenumbraApiMods Mods { get; } = mods;
|
||||
public IPenumbraApiModSettings ModSettings { get; } = modSettings;
|
||||
public IPenumbraApiPluginState PluginState { get; } = pluginState;
|
||||
public IPenumbraApiRedraw Redraw { get; } = redraw;
|
||||
public IPenumbraApiResolve Resolve { get; } = resolve;
|
||||
public IPenumbraApiResourceTree ResourceTree { get; } = resourceTree;
|
||||
public IPenumbraApiTemporary Temporary { get; } = temporary;
|
||||
public IPenumbraApiUi Ui { get; } = ui;
|
||||
}
|
||||
38
Penumbra/Api/Api/PluginStateApi.cs
Normal file
38
Penumbra/Api/Api/PluginStateApi.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Frozen;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService
|
||||
{
|
||||
public string GetModDirectory()
|
||||
=> config.ModDirectory;
|
||||
|
||||
public string GetConfiguration()
|
||||
=> JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
|
||||
public event Action<string, bool>? ModDirectoryChanged
|
||||
{
|
||||
add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
|
||||
remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public bool GetEnabledState()
|
||||
=> config.EnableMods;
|
||||
|
||||
public event Action<bool>? EnabledChange
|
||||
{
|
||||
add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
|
||||
remove => communicator.EnabledChanged.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public FrozenSet<string> SupportedFeatures
|
||||
=> FeatureChecker.SupportedFeatures.ToFrozenSet();
|
||||
|
||||
public string[] CheckSupportedFeatures(IEnumerable<string> requiredFeatures)
|
||||
=> requiredFeatures.Where(f => !FeatureChecker.Supported(f)).ToArray();
|
||||
}
|
||||
57
Penumbra/Api/Api/RedrawApi.cs
Normal file
57
Penumbra/Api/Api/RedrawApi.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
|
||||
{
|
||||
public void RedrawObject(int gameObjectIndex, RedrawType setting)
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting));
|
||||
}
|
||||
|
||||
public void RedrawObject(string name, RedrawType setting)
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting));
|
||||
}
|
||||
|
||||
public void RedrawObject(IGameObject? gameObject, RedrawType setting)
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting));
|
||||
}
|
||||
|
||||
public void RedrawAll(RedrawType setting)
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
|
||||
}
|
||||
|
||||
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
|
||||
{
|
||||
|
||||
if (!collections.Storage.ById(collectionId, out var collection))
|
||||
collection = ModCollection.Empty;
|
||||
framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var actor in objects.Objects)
|
||||
{
|
||||
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
|
||||
if (collection == modCollection)
|
||||
{
|
||||
redrawService.RedrawObject(actor.ObjectIndex, setting);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public event GameObjectRedrawnDelegate? GameObjectRedrawn
|
||||
{
|
||||
add => redrawService.GameObjectRedrawn += value;
|
||||
remove => redrawService.GameObjectRedrawn -= value;
|
||||
}
|
||||
}
|
||||
135
Penumbra/Api/Api/ResolveApi.cs
Normal file
135
Penumbra/Api/Api/ResolveApi.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class ResolveApi(
|
||||
ModManager modManager,
|
||||
CollectionManager collectionManager,
|
||||
Configuration config,
|
||||
CollectionResolver collectionResolver,
|
||||
ApiHelpers helpers,
|
||||
IFramework framework) : IPenumbraApiResolve, IApiService
|
||||
{
|
||||
public string ResolveDefaultPath(string gamePath)
|
||||
=> ResolvePath(gamePath, modManager, collectionManager.Active.Default);
|
||||
|
||||
public string ResolveInterfacePath(string gamePath)
|
||||
=> ResolvePath(gamePath, modManager, collectionManager.Active.Interface);
|
||||
|
||||
public string ResolveGameObjectPath(string gamePath, int gameObjectIdx)
|
||||
{
|
||||
helpers.AssociatedCollection(gameObjectIdx, out var collection);
|
||||
return ResolvePath(gamePath, modManager, collection);
|
||||
}
|
||||
|
||||
public string ResolvePlayerPath(string gamePath)
|
||||
=> ResolvePath(gamePath, modManager, collectionResolver.PlayerCollection());
|
||||
|
||||
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIdx)
|
||||
{
|
||||
if (!config.EnableMods)
|
||||
return [moddedPath];
|
||||
|
||||
helpers.AssociatedCollection(gameObjectIdx, out var collection);
|
||||
var ret = collection.ReverseResolvePath(new FullPath(moddedPath));
|
||||
return ret.Select(r => r.ToString()).ToArray();
|
||||
}
|
||||
|
||||
public PenumbraApiEc ResolvePath(Guid collectionId, string gamePath, out string resolvedPath)
|
||||
{
|
||||
resolvedPath = gamePath;
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return PenumbraApiEc.CollectionMissing;
|
||||
|
||||
if (!collection.HasCache)
|
||||
return PenumbraApiEc.CollectionInactive;
|
||||
|
||||
resolvedPath = ResolvePath(gamePath, modManager, collection);
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
public string[] ReverseResolvePlayerPath(string moddedPath)
|
||||
{
|
||||
if (!config.EnableMods)
|
||||
return [moddedPath];
|
||||
|
||||
var ret = collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(moddedPath));
|
||||
return ret.Select(r => r.ToString()).ToArray();
|
||||
}
|
||||
|
||||
public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse)
|
||||
{
|
||||
if (!config.EnableMods)
|
||||
return (forward, reverse.Select(p => new[]
|
||||
{
|
||||
p,
|
||||
}).ToArray());
|
||||
|
||||
var playerCollection = collectionResolver.PlayerCollection();
|
||||
var resolved = forward.Select(p => ResolvePath(p, modManager, playerCollection)).ToArray();
|
||||
var reverseResolved = playerCollection.ReverseResolvePaths(reverse);
|
||||
return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
|
||||
}
|
||||
|
||||
public PenumbraApiEc ResolvePaths(Guid collectionId, string[] forward, string[] reverse, out string[] resolvedForward,
|
||||
out string[][] resolvedReverse)
|
||||
{
|
||||
resolvedForward = forward;
|
||||
resolvedReverse = [];
|
||||
if (!config.EnableMods)
|
||||
return PenumbraApiEc.Success;
|
||||
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return PenumbraApiEc.CollectionMissing;
|
||||
|
||||
if (!collection.HasCache)
|
||||
return PenumbraApiEc.CollectionInactive;
|
||||
|
||||
resolvedForward = forward.Select(p => ResolvePath(p, modManager, collection)).ToArray();
|
||||
var reverseResolved = collection.ReverseResolvePaths(reverse);
|
||||
resolvedReverse = reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse)
|
||||
{
|
||||
if (!config.EnableMods)
|
||||
return (forward, reverse.Select(p => new[]
|
||||
{
|
||||
p,
|
||||
}).ToArray());
|
||||
|
||||
return await Task.Run(async () =>
|
||||
{
|
||||
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
|
||||
var forwardTask = Task.Run(() =>
|
||||
{
|
||||
var forwardRet = new string[forward.Length];
|
||||
Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], modManager, playerCollection));
|
||||
return forwardRet;
|
||||
}).ConfigureAwait(false);
|
||||
var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false);
|
||||
var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
|
||||
return (await forwardTask, reverseResolved);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary> Resolve a path given by string for a specific collection. </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private string ResolvePath(string path, ModManager _, ModCollection collection)
|
||||
{
|
||||
if (!config.EnableMods)
|
||||
return path;
|
||||
|
||||
var gamePath = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty;
|
||||
var ret = collection.ResolvePath(gamePath);
|
||||
return ret?.ToString() ?? path;
|
||||
}
|
||||
}
|
||||
63
Penumbra/Api/Api/ResourceTreeApi.cs
Normal file
63
Penumbra/Api/Api/ResourceTreeApi.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.ResourceTree;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectManager objects) : IPenumbraApiResourceTree, IApiService
|
||||
{
|
||||
public Dictionary<string, HashSet<string>>?[] GetGameObjectResourcePaths(params ushort[] gameObjects)
|
||||
{
|
||||
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
|
||||
var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0);
|
||||
var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
|
||||
|
||||
return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj));
|
||||
}
|
||||
|
||||
public Dictionary<ushort, Dictionary<string, HashSet<string>>> GetPlayerResourcePaths()
|
||||
{
|
||||
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly);
|
||||
return ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
|
||||
}
|
||||
|
||||
public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData,
|
||||
params ushort[] gameObjects)
|
||||
{
|
||||
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
|
||||
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
|
||||
var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
|
||||
|
||||
return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj));
|
||||
}
|
||||
|
||||
public Dictionary<ushort, GameResourceDict> GetPlayerResourcesOfType(ResourceType type,
|
||||
bool withUiData)
|
||||
{
|
||||
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
|
||||
| (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
|
||||
return ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
|
||||
}
|
||||
|
||||
public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects)
|
||||
{
|
||||
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
|
||||
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
|
||||
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
|
||||
|
||||
return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj));
|
||||
}
|
||||
|
||||
public Dictionary<ushort, JObject> GetPlayerResourceTrees(bool withUiData)
|
||||
{
|
||||
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
|
||||
| (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
|
||||
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
|
||||
|
||||
return resDictionary;
|
||||
}
|
||||
}
|
||||
338
Penumbra/Api/Api/TemporaryApi.cs
Normal file
338
Penumbra/Api/Api/TemporaryApi.cs
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class TemporaryApi(
|
||||
TempCollectionManager tempCollections,
|
||||
ObjectManager objects,
|
||||
ActorManager actors,
|
||||
CollectionManager collectionManager,
|
||||
TempModManager tempMods,
|
||||
ApiHelpers apiHelpers,
|
||||
ModManager modManager) : IPenumbraApiTemporary, IApiService
|
||||
{
|
||||
public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
|
||||
{
|
||||
if (!IdentityChecker.Check(identity))
|
||||
return (PenumbraApiEc.InvalidCredentials, Guid.Empty);
|
||||
|
||||
var collection = tempCollections.CreateTemporaryCollection(name);
|
||||
if (collection == Guid.Empty)
|
||||
return (PenumbraApiEc.UnknownError, collection);
|
||||
return (PenumbraApiEc.Success, collection);
|
||||
}
|
||||
|
||||
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
|
||||
=> tempCollections.RemoveTemporaryCollection(collectionId)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.CollectionMissing;
|
||||
|
||||
public PenumbraApiEc AssignTemporaryCollection(Guid collectionId, int actorIndex, bool forceAssignment)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ActorIndex", actorIndex, "Forced", forceAssignment);
|
||||
if (actorIndex < 0 || actorIndex >= objects.TotalCount)
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
var identifier = actors.FromObject(objects[actorIndex], out _, false, false, true);
|
||||
if (!identifier.IsValid)
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
if (!tempCollections.CollectionById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
if (forceAssignment)
|
||||
{
|
||||
if (tempCollections.Collections.ContainsKey(identifier) && !tempCollections.Collections.Delete(identifier))
|
||||
return ApiHelpers.Return(PenumbraApiEc.AssignmentDeletionFailed, args);
|
||||
}
|
||||
else if (tempCollections.Collections.ContainsKey(identifier)
|
||||
|| collectionManager.Active.Individuals.ContainsKey(identifier))
|
||||
{
|
||||
return ApiHelpers.Return(PenumbraApiEc.CharacterCollectionExists, args);
|
||||
}
|
||||
|
||||
var group = tempCollections.Collections.GetGroup(identifier);
|
||||
var ret = tempCollections.AddIdentifier(collection, group)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.UnknownError;
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary<string, string> paths, string manipString, int priority)
|
||||
{
|
||||
var args = ApiHelpers.Args("Tag", tag, "#Paths", paths.Count, "ManipString", manipString, "Priority", priority);
|
||||
if (!ConvertPaths(paths, out var p))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
|
||||
|
||||
if (!MetaApi.ConvertManips(manipString, out var m, out _))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
|
||||
|
||||
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
|
||||
{
|
||||
RedirectResult.Success => PenumbraApiEc.Success,
|
||||
_ => PenumbraApiEc.UnknownError,
|
||||
};
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc AddTemporaryMod(string tag, Guid collectionId, Dictionary<string, string> paths, string manipString, int priority)
|
||||
{
|
||||
var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "#Paths", paths.Count, "ManipString",
|
||||
manipString, "Priority", priority);
|
||||
|
||||
if (collectionId == Guid.Empty)
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
if (!tempCollections.CollectionById(collectionId, out var collection)
|
||||
&& !collectionManager.Storage.ById(collectionId, out collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
if (!ConvertPaths(paths, out var p))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
|
||||
|
||||
if (!MetaApi.ConvertManips(manipString, out var m, out _))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
|
||||
|
||||
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
|
||||
{
|
||||
RedirectResult.Success => PenumbraApiEc.Success,
|
||||
_ => PenumbraApiEc.UnknownError,
|
||||
};
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority)
|
||||
{
|
||||
var ret = tempMods.Unregister(tag, null, new ModPriority(priority)) switch
|
||||
{
|
||||
RedirectResult.Success => PenumbraApiEc.Success,
|
||||
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
|
||||
_ => PenumbraApiEc.UnknownError,
|
||||
};
|
||||
return ApiHelpers.Return(ret, ApiHelpers.Args("Tag", tag, "Priority", priority));
|
||||
}
|
||||
|
||||
public PenumbraApiEc RemoveTemporaryMod(string tag, Guid collectionId, int priority)
|
||||
{
|
||||
var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "Priority", priority);
|
||||
|
||||
if (!tempCollections.CollectionById(collectionId, out var collection)
|
||||
&& !collectionManager.Storage.ById(collectionId, out collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
var ret = tempMods.Unregister(tag, collection, new ModPriority(priority)) switch
|
||||
{
|
||||
RedirectResult.Success => PenumbraApiEc.Success,
|
||||
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
|
||||
_ => PenumbraApiEc.UnknownError,
|
||||
};
|
||||
return ApiHelpers.Return(ret, args);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, (bool, bool, int, Dictionary<string, List<string>>)?, string) QueryTemporaryModSettings(Guid collectionId,
|
||||
string modDirectory, string modName, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName);
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return (ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args), null, string.Empty);
|
||||
|
||||
return QueryTemporaryModSettings(args, collection, modDirectory, modName, key);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary<string, List<string>>)? Settings, string Source)
|
||||
QueryTemporaryModSettingsPlayer(int objectIndex,
|
||||
string modDirectory, string modName, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName);
|
||||
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
|
||||
return (ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args), null, string.Empty);
|
||||
|
||||
return QueryTemporaryModSettings(args, collection, modDirectory, modName, key);
|
||||
}
|
||||
|
||||
private (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary<string, List<string>>)? Settings, string Source) QueryTemporaryModSettings(
|
||||
in LazyString args, ModCollection collection, string modDirectory, string modName, int key)
|
||||
{
|
||||
if (!modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return (ApiHelpers.Return(PenumbraApiEc.ModMissing, args), null, string.Empty);
|
||||
|
||||
if (collection.Identity.Index <= 0)
|
||||
return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty);
|
||||
|
||||
var settings = collection.GetTempSettings(mod.Index);
|
||||
if (settings == null)
|
||||
return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty);
|
||||
|
||||
if (settings.Lock > 0 && settings.Lock != key)
|
||||
return (ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args), null, settings.Source);
|
||||
|
||||
return (ApiHelpers.Return(PenumbraApiEc.Success, args),
|
||||
(settings.ForceInherit, settings.Enabled, settings.Priority.Value, settings.ConvertToShareable(mod).Settings), settings.Source);
|
||||
}
|
||||
|
||||
|
||||
public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled,
|
||||
int priority,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> options, string source, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit,
|
||||
"Enabled", enabled,
|
||||
"Priority", priority, "Options", options, "Source", source, "Key", key);
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key);
|
||||
}
|
||||
|
||||
public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled,
|
||||
int priority,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> options, string source, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled",
|
||||
enabled,
|
||||
"Priority", priority, "Options", options, "Source", source, "Key", key);
|
||||
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key);
|
||||
}
|
||||
|
||||
private PenumbraApiEc SetTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName,
|
||||
bool inherit, bool enabled, int priority, IReadOnlyDictionary<string, IReadOnlyList<string>> options, string source, int key)
|
||||
{
|
||||
if (collection.Identity.Index <= 0)
|
||||
return ApiHelpers.Return(PenumbraApiEc.TemporarySettingImpossible, args);
|
||||
|
||||
if (!modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
|
||||
|
||||
if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key))
|
||||
if (collection.GetTempSettings(mod.Index) is { Lock: > 0 } oldSettings && oldSettings.Lock != key)
|
||||
return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args);
|
||||
|
||||
var newSettings = new TemporaryModSettings()
|
||||
{
|
||||
ForceInherit = inherit,
|
||||
Enabled = enabled,
|
||||
Priority = new ModPriority(priority),
|
||||
Lock = key,
|
||||
Source = source,
|
||||
Settings = SettingList.Default(mod),
|
||||
};
|
||||
|
||||
|
||||
foreach (var (groupName, optionNames) in options)
|
||||
{
|
||||
var ec = ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIdx, out var setting);
|
||||
if (ec != PenumbraApiEc.Success)
|
||||
return ApiHelpers.Return(ec, args);
|
||||
|
||||
newSettings.Settings[groupIdx] = setting;
|
||||
}
|
||||
|
||||
if (collectionManager.Editor.SetTemporarySettings(collection, mod, newSettings, key))
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, args);
|
||||
|
||||
// This should not happen since all error cases had been checked before.
|
||||
return ApiHelpers.Return(PenumbraApiEc.UnknownError, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key);
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key);
|
||||
}
|
||||
|
||||
public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Key", key);
|
||||
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key);
|
||||
}
|
||||
|
||||
private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key)
|
||||
{
|
||||
if (collection.Identity.Index <= 0)
|
||||
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
|
||||
|
||||
if (!modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
|
||||
|
||||
if (collection.GetTempSettings(mod.Index) is null)
|
||||
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
|
||||
|
||||
if (!collectionManager.Editor.SetTemporarySettings(collection, mod, null, key))
|
||||
return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args);
|
||||
|
||||
return ApiHelpers.Return(PenumbraApiEc.Success, args);
|
||||
}
|
||||
|
||||
public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key);
|
||||
if (!collectionManager.Storage.ById(collectionId, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
|
||||
|
||||
return RemoveAllTemporaryModSettings(args, collection, key);
|
||||
}
|
||||
|
||||
public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key)
|
||||
{
|
||||
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "Key", key);
|
||||
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
|
||||
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
|
||||
|
||||
return RemoveAllTemporaryModSettings(args, collection, key);
|
||||
}
|
||||
|
||||
private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key)
|
||||
{
|
||||
if (collection.Identity.Index <= 0)
|
||||
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
|
||||
|
||||
var numRemoved = collectionManager.Editor.ClearTemporarySettings(collection, key);
|
||||
return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Convert a dictionary of strings to a dictionary of game paths to full paths.
|
||||
/// Only returns true if all paths can successfully be converted and added.
|
||||
/// </summary>
|
||||
private static bool ConvertPaths(IReadOnlyDictionary<string, string> redirections,
|
||||
[NotNullWhen(true)] out Dictionary<Utf8GamePath, FullPath>? paths)
|
||||
{
|
||||
paths = new Dictionary<Utf8GamePath, FullPath>(redirections.Count);
|
||||
foreach (var (gString, fString) in redirections)
|
||||
{
|
||||
if (!Utf8GamePath.FromString(gString, out var path))
|
||||
{
|
||||
paths = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var fullPath = new FullPath(fString);
|
||||
if (!paths.TryAdd(path, fullPath))
|
||||
{
|
||||
paths = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
113
Penumbra/Api/Api/UiApi.cs
Normal file
113
Penumbra/Api/Api/UiApi.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.UI;
|
||||
using Penumbra.UI.Integration;
|
||||
using Penumbra.UI.Tabs;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class UiApi : IPenumbraApiUi, IApiService, IDisposable
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ConfigWindow _configWindow;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly IntegrationSettingsRegistry _integrationSettings;
|
||||
|
||||
public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager, IntegrationSettingsRegistry integrationSettings)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_configWindow = configWindow;
|
||||
_modManager = modManager;
|
||||
_integrationSettings = integrationSettings;
|
||||
_communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
|
||||
_communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ChangedItemHover.Unsubscribe(OnChangedItemHover);
|
||||
_communicator.ChangedItemClick.Unsubscribe(OnChangedItemClick);
|
||||
}
|
||||
|
||||
public event Action<ChangedItemType, uint>? ChangedItemTooltip;
|
||||
|
||||
public event Action<MouseButton, ChangedItemType, uint>? ChangedItemClicked;
|
||||
|
||||
public event Action<string, float, float>? PreSettingsTabBarDraw
|
||||
{
|
||||
add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default);
|
||||
remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public event Action<string>? PreSettingsPanelDraw
|
||||
{
|
||||
add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default);
|
||||
remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public event Action<string>? PostEnabledDraw
|
||||
{
|
||||
add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default);
|
||||
remove => _communicator.PostEnabledDraw.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public event Action<string>? PostSettingsPanelDraw
|
||||
{
|
||||
add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default);
|
||||
remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName)
|
||||
{
|
||||
_configWindow.IsOpen = true;
|
||||
if (!Enum.IsDefined(tab))
|
||||
return PenumbraApiEc.InvalidArgument;
|
||||
|
||||
if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0))
|
||||
{
|
||||
if (_modManager.TryGetMod(modDirectory, modName, out var mod))
|
||||
_communicator.SelectTab.Invoke(tab, mod);
|
||||
else
|
||||
return PenumbraApiEc.ModMissing;
|
||||
}
|
||||
else if (tab != TabType.None)
|
||||
{
|
||||
_communicator.SelectTab.Invoke(tab, null);
|
||||
}
|
||||
|
||||
return PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
public void CloseMainWindow()
|
||||
=> _configWindow.IsOpen = false;
|
||||
|
||||
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data)
|
||||
{
|
||||
if (ChangedItemClicked == null)
|
||||
return;
|
||||
|
||||
var (type, id) = data.ToApiObject();
|
||||
ChangedItemClicked.Invoke(button, type, id);
|
||||
}
|
||||
|
||||
private void OnChangedItemHover(IIdentifiedObjectData data)
|
||||
{
|
||||
if (ChangedItemTooltip == null)
|
||||
return;
|
||||
|
||||
var (type, id) = data.ToApiObject();
|
||||
ChangedItemTooltip.Invoke(type, id);
|
||||
}
|
||||
|
||||
public PenumbraApiEc RegisterSettingsSection(Action draw)
|
||||
=> _integrationSettings.RegisterSection(draw);
|
||||
|
||||
public PenumbraApiEc UnregisterSettingsSection(Action draw)
|
||||
=> _integrationSettings.UnregisterSection(draw)
|
||||
? PenumbraApiEc.Success
|
||||
: PenumbraApiEc.NothingChanged;
|
||||
}
|
||||
161
Penumbra/Api/DalamudSubstitutionProvider.cs
Normal file
161
Penumbra/Api/DalamudSubstitutionProvider.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public class DalamudSubstitutionProvider : IDisposable, IApiService
|
||||
{
|
||||
private readonly ITextureSubstitutionProvider _substitution;
|
||||
private readonly IUiBuilder _uiBuilder;
|
||||
private readonly ActiveCollectionData _activeCollectionData;
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public bool Enabled
|
||||
=> _config.UseDalamudUiTextureRedirection;
|
||||
|
||||
public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData,
|
||||
Configuration config, CommunicatorService communicator, IUiBuilder ui)
|
||||
{
|
||||
_substitution = substitution;
|
||||
_uiBuilder = ui;
|
||||
_activeCollectionData = activeCollectionData;
|
||||
_config = config;
|
||||
_communicator = communicator;
|
||||
if (Enabled)
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public void Set(bool value)
|
||||
{
|
||||
if (value)
|
||||
Enable();
|
||||
else
|
||||
Disable();
|
||||
}
|
||||
|
||||
public void ResetSubstitutions(IEnumerable<Utf8GamePath> paths)
|
||||
{
|
||||
if (!_uiBuilder.UiPrepared)
|
||||
return;
|
||||
|
||||
var transformed = paths
|
||||
.Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8))
|
||||
.Select(p => p.ToString());
|
||||
_substitution.InvalidatePaths(transformed);
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
if (Enabled)
|
||||
return;
|
||||
|
||||
_config.UseDalamudUiTextureRedirection = true;
|
||||
_config.Save();
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if (!Enabled)
|
||||
return;
|
||||
|
||||
Unsubscribe();
|
||||
_config.UseDalamudUiTextureRedirection = false;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> Unsubscribe();
|
||||
|
||||
private void OnCollectionChange(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _)
|
||||
{
|
||||
if (type is not CollectionType.Interface)
|
||||
return;
|
||||
|
||||
var enumerable = oldCollection?.ResolvedFiles.Keys ?? Array.Empty<Utf8GamePath>().AsEnumerable();
|
||||
enumerable = enumerable.Concat(newCollection?.ResolvedFiles.Keys ?? Array.Empty<Utf8GamePath>().AsEnumerable());
|
||||
ResetSubstitutions(enumerable);
|
||||
}
|
||||
|
||||
private void OnResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath key, FullPath _1, FullPath _2,
|
||||
IMod? _3)
|
||||
{
|
||||
if (_activeCollectionData.Interface != collection)
|
||||
return;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case ResolvedFileChanged.Type.Added:
|
||||
case ResolvedFileChanged.Type.Removed:
|
||||
case ResolvedFileChanged.Type.Replaced:
|
||||
ResetSubstitutions([key]);
|
||||
break;
|
||||
case ResolvedFileChanged.Type.FullRecomputeStart:
|
||||
case ResolvedFileChanged.Type.FullRecomputeFinished:
|
||||
ResetSubstitutions(collection.ResolvedFiles.Keys);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnabledChange(bool state)
|
||||
{
|
||||
if (state)
|
||||
OnCollectionChange(CollectionType.Interface, null, _activeCollectionData.Interface, string.Empty);
|
||||
else
|
||||
OnCollectionChange(CollectionType.Interface, _activeCollectionData.Interface, null, string.Empty);
|
||||
}
|
||||
|
||||
private void Substitute(string path, ref string? replacementPath)
|
||||
{
|
||||
// Do not replace when not enabled.
|
||||
if (!_config.EnableMods)
|
||||
return;
|
||||
|
||||
// Let other plugins prioritize replacement paths.
|
||||
if (replacementPath != null)
|
||||
return;
|
||||
|
||||
// Only replace interface textures.
|
||||
if (!path.StartsWith("ui/") && !path.StartsWith("common/font/"))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!Utf8GamePath.FromString(path, out var utf8Path))
|
||||
return;
|
||||
|
||||
var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path);
|
||||
replacementPath = resolved?.FullName;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
_substitution.InterceptTexDataLoad += Substitute;
|
||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.DalamudSubstitutionProvider);
|
||||
_communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.DalamudSubstitutionProvider);
|
||||
_communicator.EnabledChanged.Subscribe(OnEnabledChange, EnabledChanged.Priority.DalamudSubstitutionProvider);
|
||||
OnCollectionChange(CollectionType.Interface, null, _activeCollectionData.Interface, string.Empty);
|
||||
}
|
||||
|
||||
private void Unsubscribe()
|
||||
{
|
||||
_substitution.InterceptTexDataLoad -= Substitute;
|
||||
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
|
||||
_communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange);
|
||||
_communicator.EnabledChanged.Unsubscribe(OnEnabledChange);
|
||||
OnCollectionChange(CollectionType.Interface, _activeCollectionData.Interface, null, string.Empty);
|
||||
}
|
||||
}
|
||||
203
Penumbra/Api/HttpApi.cs
Normal file
203
Penumbra/Api/HttpApi.cs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using EmbedIO;
|
||||
using EmbedIO.Routing;
|
||||
using EmbedIO.WebApi;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public class HttpApi : IDisposable, IApiService
|
||||
{
|
||||
private partial class Controller : WebApiController
|
||||
{
|
||||
// @formatter:off
|
||||
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
|
||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
|
||||
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public const string Prefix = "http://localhost:42069/";
|
||||
|
||||
private readonly IPenumbraApi _api;
|
||||
private readonly IFramework _framework;
|
||||
private WebServer? _server;
|
||||
|
||||
public HttpApi(Configuration config, IPenumbraApi api, IFramework framework)
|
||||
{
|
||||
_api = api;
|
||||
_framework = framework;
|
||||
if (config.EnableHttpApi)
|
||||
CreateWebServer();
|
||||
}
|
||||
|
||||
public bool Enabled
|
||||
=> _server != null;
|
||||
|
||||
public void CreateWebServer()
|
||||
{
|
||||
ShutdownWebServer();
|
||||
|
||||
_server = new WebServer(o => o
|
||||
.WithUrlPrefix(Prefix)
|
||||
.WithMode(HttpListenerMode.EmbedIO))
|
||||
.WithCors(Prefix)
|
||||
.WithWebApi("/api", m => m.WithController(() => new Controller(_api, _framework)));
|
||||
|
||||
_server.StateChanged += (_, e) => Penumbra.Log.Information($"WebServer New State - {e.NewState}");
|
||||
_server.RunAsync();
|
||||
}
|
||||
|
||||
public void ShutdownWebServer()
|
||||
{
|
||||
_server?.Dispose();
|
||||
_server = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> ShutdownWebServer();
|
||||
|
||||
private partial class Controller(IPenumbraApi api, IFramework framework)
|
||||
{
|
||||
public partial string GetModDirectory()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
|
||||
return api.PluginState.GetModDirectory();
|
||||
}
|
||||
|
||||
public partial object? GetMods()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
|
||||
return api.Mods.GetModList();
|
||||
}
|
||||
|
||||
public async partial Task Redraw()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<RedrawData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] [{Environment.CurrentManagedThreadId}] {nameof(Redraw)} triggered with {data}.");
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (data.ObjectTableIndex >= 0)
|
||||
api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type);
|
||||
else
|
||||
api.Redraw.RedrawAll(data.Type);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async partial Task RedrawAll()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered.");
|
||||
await framework.RunOnFrameworkThread(() => { api.Redraw.RedrawAll(RedrawType.Redraw); }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async partial Task ReloadMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModReloadData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(ReloadMod)} triggered with {data}.");
|
||||
// Add the mod if it is not already loaded and if the directory name is given.
|
||||
// AddMod returns Success if the mod is already loaded.
|
||||
if (data.Path.Length != 0)
|
||||
api.Mods.AddMod(data.Path);
|
||||
|
||||
// Reload the mod by path or name, which will also remove no-longer existing mods.
|
||||
api.Mods.ReloadMod(data.Path, data.Name);
|
||||
}
|
||||
|
||||
public async partial Task InstallMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModInstallData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}.");
|
||||
if (data.Path.Length != 0)
|
||||
api.Mods.InstallMod(data.Path);
|
||||
}
|
||||
|
||||
public partial void OpenWindow()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
|
||||
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
public async partial Task FocusMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered.");
|
||||
if (data.Path.Length != 0)
|
||||
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
|
||||
}
|
||||
|
||||
public async partial Task SetModSettings()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
|
||||
if (data.Inherit.HasValue)
|
||||
{
|
||||
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
|
||||
if (data.Inherit.Value)
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.State.HasValue)
|
||||
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
|
||||
if (data.Priority.HasValue)
|
||||
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
|
||||
foreach (var (group, settings) in data.Settings ?? [])
|
||||
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private record ModReloadData(string Path, string Name)
|
||||
{
|
||||
public ModReloadData()
|
||||
: this(string.Empty, string.Empty)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record ModFocusData(string Path, string Name)
|
||||
{
|
||||
public ModFocusData()
|
||||
: this(string.Empty, string.Empty)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record ModInstallData(string Path)
|
||||
{
|
||||
public ModInstallData()
|
||||
: this(string.Empty)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record RedrawData(string Name, RedrawType Type, int ObjectTableIndex)
|
||||
{
|
||||
public RedrawData()
|
||||
: this(string.Empty, RedrawType.Redraw, -1)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record SetModSettingsData(
|
||||
Guid? CollectionId,
|
||||
string ModPath,
|
||||
string ModName,
|
||||
bool? Inherit,
|
||||
bool? State,
|
||||
int? Priority,
|
||||
Dictionary<string, List<string>>? Settings)
|
||||
{
|
||||
public SetModSettingsData()
|
||||
: this(null, string.Empty, string.Empty, null, null, null, null)
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Lumina.Data;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public interface IPenumbraApiBase
|
||||
{
|
||||
// The API version is staggered in two parts.
|
||||
// The major/Breaking version only increments if there are changes breaking backwards compatibility.
|
||||
// The minor/Feature version increments any time there is something added
|
||||
// and resets when Breaking is incremented.
|
||||
public (int Breaking, int Feature) ApiVersion { get; }
|
||||
public bool Valid { get; }
|
||||
}
|
||||
|
||||
public delegate void ChangedItemHover( object? item );
|
||||
public delegate void ChangedItemClick( MouseButton button, object? item );
|
||||
public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex );
|
||||
public delegate void ModSettingChanged( ModSettingChange type, string collectionName, string modDirectory, bool inherited );
|
||||
|
||||
public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize,
|
||||
IntPtr equipData );
|
||||
|
||||
public enum PenumbraApiEc
|
||||
{
|
||||
Success = 0,
|
||||
NothingChanged = 1,
|
||||
CollectionMissing = 2,
|
||||
ModMissing = 3,
|
||||
OptionGroupMissing = 4,
|
||||
OptionMissing = 5,
|
||||
|
||||
CharacterCollectionExists = 6,
|
||||
LowerPriority = 7,
|
||||
InvalidGamePath = 8,
|
||||
FileMissing = 9,
|
||||
InvalidManipulation = 10,
|
||||
InvalidArgument = 11,
|
||||
UnknownError = 255,
|
||||
}
|
||||
|
||||
public interface IPenumbraApi : IPenumbraApiBase
|
||||
{
|
||||
// Obtain the currently set mod directory from the configuration.
|
||||
public string GetModDirectory();
|
||||
|
||||
// Obtain the entire current penumbra configuration as a json encoded string.
|
||||
public string GetConfiguration();
|
||||
|
||||
// Triggered when the user hovers over a listed changed object in a mod tab.
|
||||
// Can be used to append tooltips.
|
||||
public event ChangedItemHover? ChangedItemTooltip;
|
||||
|
||||
// Events that are fired before and after the content of a mod settings panel are drawn.
|
||||
// Both are fired inside the child window of the settings panel itself.
|
||||
public event Action< string >? PreSettingsPanelDraw;
|
||||
public event Action< string >? PostSettingsPanelDraw;
|
||||
|
||||
// Triggered when the user clicks a listed changed object in a mod tab.
|
||||
public event ChangedItemClick? ChangedItemClicked;
|
||||
public event GameObjectRedrawn? GameObjectRedrawn;
|
||||
|
||||
// Triggered when a character base is created and a corresponding gameObject could be found,
|
||||
// before the Draw Object is actually created, so customize and equipdata can be manipulated beforehand.
|
||||
public event CreatingCharacterBaseDelegate? CreatingCharacterBase;
|
||||
|
||||
// Queue redrawing of all actors of the given name with the given RedrawType.
|
||||
public void RedrawObject( string name, RedrawType setting );
|
||||
|
||||
// Queue redrawing of the specific actor with the given RedrawType. Should only be used when the actor is sure to be valid.
|
||||
public void RedrawObject( GameObject gameObject, RedrawType setting );
|
||||
|
||||
// Queue redrawing of the actor with the given object table index, if it exists, with the given RedrawType.
|
||||
public void RedrawObject( int tableIndex, RedrawType setting );
|
||||
|
||||
// Queue redrawing of all currently available actors with the given RedrawType.
|
||||
public void RedrawAll( RedrawType setting );
|
||||
|
||||
// Resolve a given gamePath via Penumbra using the Default and Forced collections.
|
||||
// Returns the given gamePath if penumbra would not manipulate it.
|
||||
public string ResolvePath( string gamePath );
|
||||
|
||||
// Resolve a given gamePath via Penumbra using the character collection for the given name (if it exists) and the Forced collections.
|
||||
// Returns the given gamePath if penumbra would not manipulate it.
|
||||
public string ResolvePath( string gamePath, string characterName );
|
||||
|
||||
// Resolve a given gamePath via Penumbra using any applicable character collections for the current character.
|
||||
// Returns the given gamePath if penumbra would not manipulate it.
|
||||
public string ResolvePlayerPath( string gamePath );
|
||||
|
||||
// Reverse resolves a given modded local path into its replacement in form of all applicable game paths for given character collection.
|
||||
public string[] ReverseResolvePath( string moddedPath, string characterName );
|
||||
|
||||
// Reverse resolves a given modded local path into its replacement in form of all applicable game paths
|
||||
// using the collection applying to the player character.
|
||||
public string[] ReverseResolvePlayerPath( string moddedPath );
|
||||
|
||||
// Try to load a given gamePath with the resolved path from Penumbra.
|
||||
public T? GetFile< T >( string gamePath ) where T : FileResource;
|
||||
|
||||
// Try to load a given gamePath with the resolved path from Penumbra.
|
||||
public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource;
|
||||
|
||||
// Gets a dictionary of effected items from a collection
|
||||
public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName );
|
||||
|
||||
// Obtain a list of the names of all currently installed collections.
|
||||
public IList< string > GetCollections();
|
||||
|
||||
// Obtain the name of the currently selected collection.
|
||||
public string GetCurrentCollection();
|
||||
|
||||
// Obtain the name of the default collection.
|
||||
public string GetDefaultCollection();
|
||||
|
||||
// Obtain the name of the collection associated with characterName and whether it is configured or inferred from default.
|
||||
public (string, bool) GetCharacterCollection( string characterName );
|
||||
|
||||
// Obtain the game object associated with a given draw object and the name of the collection associated with this game object.
|
||||
public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject );
|
||||
|
||||
// Obtain a list of all installed mods. The first string is their directory name, the second string is their mod name.
|
||||
public IList< (string, string) > GetModList();
|
||||
|
||||
// Try to reload an existing mod by its directory name or mod name.
|
||||
// Can return ModMissing or success.
|
||||
// Reload is the same as if triggered by button press and might delete the mod if it is not valid anymore.
|
||||
public PenumbraApiEc ReloadMod( string modDirectory, string modName );
|
||||
|
||||
// Try to add a new mod inside the mod root directory (modDirectory should only be the name, not the full name).
|
||||
// Returns FileMissing if the directory does not exist or success otherwise.
|
||||
// Note that success does only imply a successful call, not a successful mod load.
|
||||
public PenumbraApiEc AddMod( string modDirectory );
|
||||
|
||||
// Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations
|
||||
// for the collection currently associated with the player.
|
||||
public string GetPlayerMetaManipulations();
|
||||
|
||||
// Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations
|
||||
// for the given collection associated with the character name, or the default collection.
|
||||
public string GetMetaManipulations( string characterName );
|
||||
|
||||
|
||||
// ############## Mod Settings #################
|
||||
|
||||
// Obtain the potential settings of a mod specified by its directory name first or mod name second.
|
||||
// Returns null if the mod could not be found.
|
||||
public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName );
|
||||
|
||||
// Obtain the enabled state, the priority, the settings of a mod specified by its directory name first or mod name second,
|
||||
// and whether these settings are inherited, or null if the collection does not set them at all.
|
||||
// If allowInheritance is false, only the collection itself will be checked.
|
||||
public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName,
|
||||
string modDirectory, string modName, bool allowInheritance );
|
||||
|
||||
// Try to set the inheritance state in the given collection of a mod specified by its directory name first or mod name second.
|
||||
// Returns Okay, NothingChanged, CollectionMissing or ModMissing.
|
||||
public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit );
|
||||
|
||||
// Try to set the enabled state in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance.
|
||||
// Returns Okay, NothingChanged, CollectionMissing or ModMissing.
|
||||
public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled );
|
||||
|
||||
// Try to set the priority in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance.
|
||||
// Returns Okay, NothingChanged, CollectionMissing or ModMissing.
|
||||
public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority );
|
||||
|
||||
// Try to set a specific option group in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance.
|
||||
// If the group is a Single Selection group, options should be a single string, otherwise the array of enabled options.
|
||||
// Returns Okay, NothingChanged, CollectionMissing or ModMissing, OptionGroupMissing or SettingMissing.
|
||||
// If any setting can not be found, it will not change anything.
|
||||
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option );
|
||||
|
||||
public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName,
|
||||
IReadOnlyList< string > options );
|
||||
|
||||
// This event gets fired when any setting in any collection changes.
|
||||
public event ModSettingChanged? ModSettingChanged;
|
||||
|
||||
// Create a temporary collection without actual settings but with a cache.
|
||||
// If no character collection for this character exists or forceOverwriteCharacter is true,
|
||||
// associate this collection to a specific character.
|
||||
// Can return Okay, CharacterCollectionExists or NothingChanged, as well as the name of the new temporary collection on success.
|
||||
public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter );
|
||||
|
||||
// Remove the temporary collection associated with characterName if it exists.
|
||||
// Can return Okay or NothingChanged.
|
||||
public PenumbraApiEc RemoveTemporaryCollection( string characterName );
|
||||
|
||||
// Set a temporary mod with the given paths, manipulations and priority and the name tag to all collections.
|
||||
// Can return Okay, InvalidGamePath, or InvalidManipulation.
|
||||
public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority );
|
||||
|
||||
// Set a temporary mod with the given paths, manipulations and priority and the name tag to the collection with the given name, which can be temporary.
|
||||
// Can return Okay, MissingCollection InvalidGamePath, or InvalidManipulation.
|
||||
public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, string manipString,
|
||||
int priority );
|
||||
|
||||
// Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists.
|
||||
// Can return Okay or NothingDone.
|
||||
public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority );
|
||||
|
||||
// Remove the temporary mod with the given tag and priority from the temporary mods applying to the collection of the given name, which can be temporary.
|
||||
// Can return Okay or NothingDone.
|
||||
public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority );
|
||||
}
|
||||
28
Penumbra/Api/IpcLaunchingProvider.cs
Normal file
28
Penumbra/Api/IpcLaunchingProvider.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Dalamud.Plugin;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public sealed class IpcLaunchingProvider : IApiService
|
||||
{
|
||||
public IpcLaunchingProvider(IDalamudPluginInterface pi, Logger log)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var subscriber = log.MainLogger.IsEnabled(LogEventLevel.Debug)
|
||||
? IpcSubscribers.Launching.Subscriber(pi,
|
||||
(major, minor) => log.Debug($"[IPC] Invoked Penumbra.Launching IPC with API Version {major}.{minor}."))
|
||||
: null;
|
||||
|
||||
using var provider = IpcSubscribers.Launching.Provider(pi);
|
||||
provider.Invoke(PenumbraApi.BreakingVersion, PenumbraApi.FeatureVersion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error($"[IPC] Could not invoke Penumbra.Launching IPC:\n{ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
161
Penumbra/Api/IpcProviders.cs
Normal file
161
Penumbra/Api/IpcProviders.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
using Dalamud.Plugin;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Communication;
|
||||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public sealed class IpcProviders : IDisposable, IApiService
|
||||
{
|
||||
private readonly List<IDisposable> _providers;
|
||||
|
||||
private readonly EventProvider _disposedProvider;
|
||||
private readonly EventProvider _initializedProvider;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
|
||||
public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api, CharacterUtility characterUtility)
|
||||
{
|
||||
_characterUtility = characterUtility;
|
||||
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
|
||||
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
|
||||
_providers =
|
||||
[
|
||||
IpcSubscribers.GetCollections.Provider(pi, api.Collection),
|
||||
IpcSubscribers.GetCollectionsByIdentifier.Provider(pi, api.Collection),
|
||||
IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection),
|
||||
IpcSubscribers.GetCollection.Provider(pi, api.Collection),
|
||||
IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection),
|
||||
IpcSubscribers.SetCollection.Provider(pi, api.Collection),
|
||||
IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection),
|
||||
IpcSubscribers.CheckCurrentChangedItemFunc.Provider(pi, api.Collection),
|
||||
|
||||
IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing),
|
||||
IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing),
|
||||
|
||||
IpcSubscribers.GetDrawObjectInfo.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GetCutsceneParentIndex.Provider(pi, api.GameState),
|
||||
IpcSubscribers.SetCutsceneParentIndex.Provider(pi, api.GameState),
|
||||
IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState),
|
||||
IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState),
|
||||
|
||||
IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta),
|
||||
IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta),
|
||||
|
||||
IpcSubscribers.GetModList.Provider(pi, api.Mods),
|
||||
IpcSubscribers.InstallMod.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ReloadMod.Provider(pi, api.Mods),
|
||||
IpcSubscribers.AddMod.Provider(pi, api.Mods),
|
||||
IpcSubscribers.DeleteMod.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ModMoved.Provider(pi, api.Mods),
|
||||
IpcSubscribers.CreatingPcp.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ParsingPcp.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetModPath.Provider(pi, api.Mods),
|
||||
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetChangedItemAdapterDictionary.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetChangedItemAdapterList.Provider(pi, api.Mods),
|
||||
|
||||
IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.GetSettingsInAllCollections.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TrySetModSetting.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.TrySetModSettings.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.ModSettingChanged.Provider(pi, api.ModSettings),
|
||||
IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings),
|
||||
|
||||
IpcSubscribers.ApiVersion.Provider(pi, api),
|
||||
new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility
|
||||
new FuncProvider<int>(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility
|
||||
IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
|
||||
|
||||
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
|
||||
|
||||
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolveGameObjectPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePlayerPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ReverseResolveGameObjectPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolvePaths.Provider(pi, api.Resolve),
|
||||
|
||||
IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree),
|
||||
IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree),
|
||||
IpcSubscribers.GetGameObjectResourcesOfType.Provider(pi, api.ResourceTree),
|
||||
IpcSubscribers.GetPlayerResourcesOfType.Provider(pi, api.ResourceTree),
|
||||
IpcSubscribers.GetGameObjectResourceTrees.Provider(pi, api.ResourceTree),
|
||||
IpcSubscribers.GetPlayerResourceTrees.Provider(pi, api.ResourceTree),
|
||||
|
||||
IpcSubscribers.CreateTemporaryCollection.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.DeleteTemporaryCollection.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.AssignTemporaryCollection.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.AddTemporaryModAll.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.SetTemporaryModSettings.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.SetTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.RemoveTemporaryModSettings.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.QueryTemporaryModSettings.Provider(pi, api.Temporary),
|
||||
IpcSubscribers.QueryTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
|
||||
|
||||
IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui),
|
||||
IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui),
|
||||
IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui),
|
||||
IpcSubscribers.PreSettingsDraw.Provider(pi, api.Ui),
|
||||
IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui),
|
||||
IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui),
|
||||
IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
|
||||
IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
|
||||
IpcSubscribers.RegisterSettingsSection.Provider(pi, api.Ui),
|
||||
IpcSubscribers.UnregisterSettingsSection.Provider(pi, api.Ui),
|
||||
];
|
||||
if (_characterUtility.Ready)
|
||||
_initializedProvider.Invoke();
|
||||
else
|
||||
_characterUtility.LoadingFinished.Subscribe(OnCharacterUtilityReady, CharacterUtilityFinished.Priority.IpcProvider);
|
||||
}
|
||||
|
||||
private void OnCharacterUtilityReady()
|
||||
{
|
||||
_initializedProvider.Invoke();
|
||||
_characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady);
|
||||
foreach (var provider in _providers)
|
||||
provider.Dispose();
|
||||
_providers.Clear();
|
||||
_initializedProvider.Dispose();
|
||||
_disposedProvider.Invoke();
|
||||
_disposedProvider.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,904 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public class IpcTester : IDisposable
|
||||
{
|
||||
private readonly PenumbraIpc _ipc;
|
||||
private readonly DalamudPluginInterface _pi;
|
||||
|
||||
private readonly ICallGateSubscriber< object? > _initialized;
|
||||
private readonly ICallGateSubscriber< object? > _disposed;
|
||||
private readonly ICallGateSubscriber< string, object? > _preSettingsDraw;
|
||||
private readonly ICallGateSubscriber< string, object? > _postSettingsDraw;
|
||||
private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn;
|
||||
private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged;
|
||||
private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreated;
|
||||
|
||||
private readonly List< DateTimeOffset > _initializedList = new();
|
||||
private readonly List< DateTimeOffset > _disposedList = new();
|
||||
|
||||
public IpcTester( DalamudPluginInterface pi, PenumbraIpc ipc )
|
||||
{
|
||||
_ipc = ipc;
|
||||
_pi = pi;
|
||||
_initialized = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderInitialized );
|
||||
_disposed = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderDisposed );
|
||||
_redrawn = _pi.GetIpcSubscriber< IntPtr, int, object? >( PenumbraIpc.LabelProviderGameObjectRedrawn );
|
||||
_preSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPreSettingsDraw );
|
||||
_postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw );
|
||||
_settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged );
|
||||
_characterBaseCreated =
|
||||
_pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase );
|
||||
_initialized.Subscribe( AddInitialized );
|
||||
_disposed.Subscribe( AddDisposed );
|
||||
_redrawn.Subscribe( SetLastRedrawn );
|
||||
_preSettingsDraw.Subscribe( UpdateLastDrawnMod );
|
||||
_postSettingsDraw.Subscribe( UpdateLastDrawnMod );
|
||||
_settingChanged.Subscribe( UpdateLastModSetting );
|
||||
_characterBaseCreated.Subscribe( UpdateLastCreated );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_initialized.Unsubscribe( AddInitialized );
|
||||
_disposed.Unsubscribe( AddDisposed );
|
||||
_redrawn.Subscribe( SetLastRedrawn );
|
||||
_tooltip?.Unsubscribe( AddedTooltip );
|
||||
_click?.Unsubscribe( AddedClick );
|
||||
_preSettingsDraw.Unsubscribe( UpdateLastDrawnMod );
|
||||
_postSettingsDraw.Unsubscribe( UpdateLastDrawnMod );
|
||||
_settingChanged.Unsubscribe( UpdateLastModSetting );
|
||||
_characterBaseCreated.Unsubscribe( UpdateLastCreated );
|
||||
}
|
||||
|
||||
private void AddInitialized()
|
||||
=> _initializedList.Add( DateTimeOffset.UtcNow );
|
||||
|
||||
private void AddDisposed()
|
||||
=> _disposedList.Add( DateTimeOffset.UtcNow );
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
try
|
||||
{
|
||||
DrawAvailable();
|
||||
DrawGeneral();
|
||||
DrawResolve();
|
||||
DrawRedraw();
|
||||
DrawChangedItems();
|
||||
DrawData();
|
||||
DrawSetting();
|
||||
DrawTemp();
|
||||
DrawTempCollections();
|
||||
DrawTempMods();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error during IPC Tests:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAvailable()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "Availability" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted( $"API Version: {_ipc.Api.ApiVersion.Breaking}.{_ipc.Api.ApiVersion.Feature:D4}" );
|
||||
ImGui.TextUnformatted( "Available subscriptions:" );
|
||||
using var indent = ImRaii.PushIndent();
|
||||
|
||||
var dict = _ipc.GetType().GetFields( BindingFlags.Static | BindingFlags.Public ).Where( f => f.IsLiteral )
|
||||
.ToDictionary( f => f.Name, f => f.GetValue( _ipc ) as string );
|
||||
foreach( var provider in _ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) )
|
||||
{
|
||||
var value = provider.GetValue( _ipc );
|
||||
if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label ) )
|
||||
{
|
||||
ImGui.TextUnformatted( label );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawIntro( string label, string info )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( label );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( info );
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
|
||||
private string _currentConfiguration = string.Empty;
|
||||
private string _lastDrawnMod = string.Empty;
|
||||
private DateTimeOffset _lastDrawnModTime;
|
||||
|
||||
private void UpdateLastDrawnMod( string name )
|
||||
=> ( _lastDrawnMod, _lastDrawnModTime ) = ( name, DateTimeOffset.Now );
|
||||
|
||||
private void DrawGeneral()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "General IPC" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
|
||||
|
||||
void DrawList( string label, string text, List< DateTimeOffset > list )
|
||||
{
|
||||
DrawIntro( label, text );
|
||||
if( list.Count == 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( "Never" );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted( list[ ^1 ].LocalDateTime.ToString( CultureInfo.CurrentCulture ) );
|
||||
if( list.Count > 1 && ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( string.Join( "\n",
|
||||
list.SkipLast( 1 ).Select( t => t.LocalDateTime.ToString( CultureInfo.CurrentCulture ) ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawList( PenumbraIpc.LabelProviderInitialized, "Last Initialized", _initializedList );
|
||||
DrawList( PenumbraIpc.LabelProviderDisposed, "Last Disposed", _disposedList );
|
||||
DrawIntro( PenumbraIpc.LabelProviderPostSettingsDraw, "Last Drawn Mod" );
|
||||
ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" );
|
||||
DrawIntro( PenumbraIpc.LabelProviderApiVersions, "Current Version" );
|
||||
var (breaking, features) = _pi.GetIpcSubscriber< (int, int) >( PenumbraIpc.LabelProviderApiVersions ).InvokeFunc();
|
||||
ImGui.TextUnformatted( $"{breaking}.{features:D4}" );
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" );
|
||||
ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() );
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" );
|
||||
if( ImGui.Button( "Get" ) )
|
||||
{
|
||||
_currentConfiguration = _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetConfiguration ).InvokeFunc();
|
||||
ImGui.OpenPopup( "Config Popup" );
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) );
|
||||
using var popup = ImRaii.Popup( "Config Popup" );
|
||||
if( popup )
|
||||
{
|
||||
using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( _currentConfiguration );
|
||||
}
|
||||
|
||||
if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() )
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _currentResolvePath = string.Empty;
|
||||
private string _currentResolveCharacter = string.Empty;
|
||||
private string _currentDrawObjectString = string.Empty;
|
||||
private string _currentReversePath = string.Empty;
|
||||
private IntPtr _currentDrawObject = IntPtr.Zero;
|
||||
private string _lastCreatedGameObjectName = string.Empty;
|
||||
private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
|
||||
|
||||
private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 )
|
||||
{
|
||||
var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject;
|
||||
_lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString();
|
||||
_lastCreatedGameObjectTime = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
private void DrawResolve()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "Resolve IPC" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.InputTextWithHint( "##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength );
|
||||
ImGui.InputTextWithHint( "##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32 );
|
||||
ImGui.InputTextWithHint( "##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath,
|
||||
Utf8GamePath.MaxGamePathLength );
|
||||
if( ImGui.InputTextWithHint( "##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16,
|
||||
ImGuiInputTextFlags.CharsHexadecimal ) )
|
||||
{
|
||||
_currentDrawObject = IntPtr.TryParse( _currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tmp )
|
||||
? tmp
|
||||
: IntPtr.Zero;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderResolveDefault, "Default Collection Resolve" );
|
||||
if( _currentResolvePath.Length != 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderResolveDefault )
|
||||
.InvokeFunc( _currentResolvePath ) );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderResolveCharacter, "Character Collection Resolve" );
|
||||
if( _currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string, string >( PenumbraIpc.LabelProviderResolveCharacter )
|
||||
.InvokeFunc( _currentResolvePath, _currentResolveCharacter ) );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetDrawObjectInfo, "Draw Object Info" );
|
||||
if( _currentDrawObject == IntPtr.Zero )
|
||||
{
|
||||
ImGui.TextUnformatted( "Invalid" );
|
||||
}
|
||||
else
|
||||
{
|
||||
var (ptr, collection) = _pi.GetIpcSubscriber< IntPtr, (IntPtr, string) >( PenumbraIpc.LabelProviderGetDrawObjectInfo )
|
||||
.InvokeFunc( _currentDrawObject );
|
||||
ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderReverseResolvePath, "Reversed Game Paths" );
|
||||
if( _currentReversePath.Length > 0 )
|
||||
{
|
||||
var list = _pi.GetIpcSubscriber< string, string, string[] >( PenumbraIpc.LabelProviderReverseResolvePath )
|
||||
.InvokeFunc( _currentReversePath, _currentResolveCharacter );
|
||||
if( list.Length > 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( list[ 0 ] );
|
||||
if( list.Length > 1 && ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderReverseResolvePlayerPath, "Reversed Game Paths (Player)" );
|
||||
if( _currentReversePath.Length > 0 )
|
||||
{
|
||||
var list = _pi.GetIpcSubscriber< string, string[] >( PenumbraIpc.LabelProviderReverseResolvePlayerPath )
|
||||
.InvokeFunc( _currentReversePath );
|
||||
if( list.Length > 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( list[ 0 ] );
|
||||
if( list.Length > 1 && ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderCreatingCharacterBase, "Last Drawobject created" );
|
||||
if( _lastCreatedGameObjectTime < DateTimeOffset.Now )
|
||||
{
|
||||
ImGui.TextUnformatted( $"for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" );
|
||||
}
|
||||
}
|
||||
|
||||
private string _redrawName = string.Empty;
|
||||
private int _redrawIndex = 0;
|
||||
private string _lastRedrawnString = "None";
|
||||
|
||||
private void SetLastRedrawn( IntPtr address, int index )
|
||||
{
|
||||
if( index < 0 || index > Dalamud.Objects.Length || address == IntPtr.Zero || Dalamud.Objects[ index ]?.Address != address )
|
||||
{
|
||||
_lastRedrawnString = "Invalid";
|
||||
}
|
||||
|
||||
_lastRedrawnString = $"{Dalamud.Objects[ index ]!.Name} (0x{address:X}, {index})";
|
||||
}
|
||||
|
||||
private void DrawRedraw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "Redraw IPC" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderRedrawName, "Redraw by Name" );
|
||||
ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale );
|
||||
ImGui.InputTextWithHint( "##redrawName", "Name...", ref _redrawName, 32 );
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Redraw##Name" ) )
|
||||
{
|
||||
_pi.GetIpcSubscriber< string, int, object? >( PenumbraIpc.LabelProviderRedrawName )
|
||||
.InvokeAction( _redrawName, ( int )RedrawType.Redraw );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderRedrawObject, "Redraw Player Character" );
|
||||
if( ImGui.Button( "Redraw##pc" ) && Dalamud.ClientState.LocalPlayer != null )
|
||||
{
|
||||
_pi.GetIpcSubscriber< GameObject, int, object? >( PenumbraIpc.LabelProviderRedrawObject )
|
||||
.InvokeAction( Dalamud.ClientState.LocalPlayer, ( int )RedrawType.Redraw );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderRedrawIndex, "Redraw by Index" );
|
||||
var tmp = _redrawIndex;
|
||||
ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, Dalamud.Objects.Length ) )
|
||||
{
|
||||
_redrawIndex = Math.Clamp( tmp, 0, Dalamud.Objects.Length );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Redraw##Index" ) )
|
||||
{
|
||||
_pi.GetIpcSubscriber< int, int, object? >( PenumbraIpc.LabelProviderRedrawIndex )
|
||||
.InvokeAction( _redrawIndex, ( int )RedrawType.Redraw );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderRedrawAll, "Redraw All" );
|
||||
if( ImGui.Button( "Redraw##All" ) )
|
||||
{
|
||||
_pi.GetIpcSubscriber< int, object? >( PenumbraIpc.LabelProviderRedrawAll ).InvokeAction( ( int )RedrawType.Redraw );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderGameObjectRedrawn, "Last Redrawn Object:" );
|
||||
ImGui.TextUnformatted( _lastRedrawnString );
|
||||
}
|
||||
|
||||
private bool _subscribedToTooltip = false;
|
||||
private bool _subscribedToClick = false;
|
||||
private string _changedItemCollection = string.Empty;
|
||||
private IReadOnlyDictionary< string, object? > _changedItems = new Dictionary< string, object? >();
|
||||
private string _lastClicked = string.Empty;
|
||||
private string _lastHovered = string.Empty;
|
||||
private ICallGateSubscriber< ChangedItemType, uint, object? >? _tooltip;
|
||||
private ICallGateSubscriber< MouseButton, ChangedItemType, uint, object? >? _click;
|
||||
|
||||
private void DrawChangedItems()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "Changed Item IPC" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderChangedItemTooltip, "Add Tooltip" );
|
||||
if( ImGui.Checkbox( "##tooltip", ref _subscribedToTooltip ) )
|
||||
{
|
||||
_tooltip = _pi.GetIpcSubscriber< ChangedItemType, uint, object? >( PenumbraIpc.LabelProviderChangedItemTooltip );
|
||||
if( _subscribedToTooltip )
|
||||
{
|
||||
_tooltip.Subscribe( AddedTooltip );
|
||||
}
|
||||
else
|
||||
{
|
||||
_tooltip.Unsubscribe( AddedTooltip );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( _lastHovered );
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderChangedItemClick, "Subscribe Click" );
|
||||
if( ImGui.Checkbox( "##click", ref _subscribedToClick ) )
|
||||
{
|
||||
_click = _pi.GetIpcSubscriber< MouseButton, ChangedItemType, uint, object? >( PenumbraIpc.LabelProviderChangedItemClick );
|
||||
if( _subscribedToClick )
|
||||
{
|
||||
_click.Subscribe( AddedClick );
|
||||
}
|
||||
else
|
||||
{
|
||||
_click.Unsubscribe( AddedClick );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( _lastClicked );
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetChangedItems, "Changed Item List" );
|
||||
ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale );
|
||||
ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 );
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Get" ) )
|
||||
{
|
||||
_changedItems = _pi.GetIpcSubscriber< string, IReadOnlyDictionary< string, object? > >( PenumbraIpc.LabelProviderGetChangedItems )
|
||||
.InvokeFunc( _changedItemCollection );
|
||||
ImGui.OpenPopup( "Changed Item List" );
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) );
|
||||
using var p = ImRaii.Popup( "Changed Item List" );
|
||||
if( p )
|
||||
{
|
||||
foreach( var item in _changedItems )
|
||||
{
|
||||
ImGui.TextUnformatted( item.Key );
|
||||
}
|
||||
|
||||
if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() )
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddedTooltip( ChangedItemType type, uint id )
|
||||
{
|
||||
_lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}";
|
||||
ImGui.TextUnformatted( "IPC Test Successful" );
|
||||
}
|
||||
|
||||
private void AddedClick( MouseButton button, ChangedItemType type, uint id )
|
||||
{
|
||||
_lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}";
|
||||
}
|
||||
|
||||
private string _characterCollectionName = string.Empty;
|
||||
private IList< (string, string) > _mods = new List< (string, string) >();
|
||||
private IList< string > _collections = new List< string >();
|
||||
private bool _collectionMode = false;
|
||||
|
||||
private void DrawData()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "Data IPC" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderCurrentCollectionName, "Current Collection" );
|
||||
ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderCurrentCollectionName ).InvokeFunc() );
|
||||
DrawIntro( PenumbraIpc.LabelProviderDefaultCollectionName, "Default Collection" );
|
||||
ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderDefaultCollectionName ).InvokeFunc() );
|
||||
DrawIntro( PenumbraIpc.LabelProviderCharacterCollectionName, "Character" );
|
||||
ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale );
|
||||
ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 );
|
||||
var (c, s) = _pi.GetIpcSubscriber< string, (string, bool) >( PenumbraIpc.LabelProviderCharacterCollectionName )
|
||||
.InvokeFunc( _characterCollectionName );
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( $"{c}, {( s ? "Custom" : "Default" )}" );
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetCollections, "Collections" );
|
||||
if( ImGui.Button( "Get##Collections" ) )
|
||||
{
|
||||
_collectionMode = true;
|
||||
_collections = _pi.GetIpcSubscriber< IList< string > >( PenumbraIpc.LabelProviderGetCollections ).InvokeFunc();
|
||||
ImGui.OpenPopup( "Ipc Data" );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetMods, "Mods" );
|
||||
if( ImGui.Button( "Get##Mods" ) )
|
||||
{
|
||||
_collectionMode = false;
|
||||
_mods = _pi.GetIpcSubscriber< IList< (string, string) > >( PenumbraIpc.LabelProviderGetMods ).InvokeFunc();
|
||||
ImGui.OpenPopup( "Ipc Data" );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetMetaManipulations, "Meta Manipulations" );
|
||||
if( ImGui.Button( "Copy to Clipboard" ) )
|
||||
{
|
||||
var base64 = _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderGetMetaManipulations )
|
||||
.InvokeFunc( _characterCollectionName );
|
||||
ImGui.SetClipboardText( base64 );
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) );
|
||||
using var p = ImRaii.Popup( "Ipc Data" );
|
||||
if( p )
|
||||
{
|
||||
if( _collectionMode )
|
||||
{
|
||||
foreach( var collection in _collections )
|
||||
{
|
||||
ImGui.TextUnformatted( collection );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( var (modDir, modName) in _mods )
|
||||
{
|
||||
ImGui.TextUnformatted( $"{modDir}: {modName}" );
|
||||
}
|
||||
}
|
||||
|
||||
if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() )
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _settingsModDirectory = string.Empty;
|
||||
private string _settingsModName = string.Empty;
|
||||
private string _settingsCollection = string.Empty;
|
||||
private bool _settingsAllowInheritance = true;
|
||||
private bool _settingsInherit = false;
|
||||
private bool _settingsEnabled = false;
|
||||
private int _settingsPriority = 0;
|
||||
private IDictionary< string, (IList< string >, SelectType) >? _availableSettings;
|
||||
private IDictionary< string, IList< string > >? _currentSettings = null;
|
||||
private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
|
||||
private ModSettingChange _lastSettingChangeType;
|
||||
private string _lastSettingChangeCollection = string.Empty;
|
||||
private string _lastSettingChangeMod = string.Empty;
|
||||
private bool _lastSettingChangeInherited;
|
||||
private DateTimeOffset _lastSettingChange;
|
||||
private PenumbraApiEc _lastReloadEc = PenumbraApiEc.Success;
|
||||
|
||||
|
||||
private void UpdateLastModSetting( ModSettingChange type, string collection, string mod, bool inherited )
|
||||
{
|
||||
_lastSettingChangeType = type;
|
||||
_lastSettingChangeCollection = collection;
|
||||
_lastSettingChangeMod = mod;
|
||||
_lastSettingChangeInherited = inherited;
|
||||
_lastSettingChange = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
private void DrawSetting()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "Settings IPC" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.InputTextWithHint( "##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100 );
|
||||
ImGui.InputTextWithHint( "##settingsName", "Mod Name...", ref _settingsModName, 100 );
|
||||
ImGui.InputTextWithHint( "##settingsCollection", "Collection...", ref _settingsCollection, 100 );
|
||||
ImGui.Checkbox( "Allow Inheritance", ref _settingsAllowInheritance );
|
||||
|
||||
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIntro( "Last Error", _lastSettingsError.ToString() );
|
||||
DrawIntro( PenumbraIpc.LabelProviderModSettingChanged, "Last Mod Setting Changed" );
|
||||
ImGui.TextUnformatted( _lastSettingChangeMod.Length > 0
|
||||
? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{( _lastSettingChangeInherited ? " (Inherited)" : string.Empty )} at {_lastSettingChange}"
|
||||
: "None" );
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetAvailableModSettings, "Get Available Settings" );
|
||||
if( ImGui.Button( "Get##Available" ) )
|
||||
{
|
||||
_availableSettings = _pi
|
||||
.GetIpcSubscriber< string, string, IDictionary< string, (IList< string >, SelectType) >? >(
|
||||
PenumbraIpc.LabelProviderGetAvailableModSettings ).InvokeFunc( _settingsModDirectory, _settingsModName );
|
||||
_lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderReloadMod, "Reload Mod" );
|
||||
if( ImGui.Button( "Reload" ) )
|
||||
{
|
||||
_lastReloadEc = _pi.GetIpcSubscriber< string, string, PenumbraApiEc >( PenumbraIpc.LabelProviderReloadMod )
|
||||
.InvokeFunc( _settingsModDirectory, _settingsModName );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( _lastReloadEc.ToString() );
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderGetCurrentModSettings, "Get Current Settings" );
|
||||
if( ImGui.Button( "Get##Current" ) )
|
||||
{
|
||||
var ret = _pi
|
||||
.GetIpcSubscriber< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >(
|
||||
PenumbraIpc.LabelProviderGetCurrentModSettings ).InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName,
|
||||
_settingsAllowInheritance );
|
||||
_lastSettingsError = ret.Item1;
|
||||
if( ret.Item1 == PenumbraApiEc.Success )
|
||||
{
|
||||
_settingsEnabled = ret.Item2?.Item1 ?? false;
|
||||
_settingsInherit = ret.Item2?.Item4 ?? false;
|
||||
_settingsPriority = ret.Item2?.Item2 ?? 0;
|
||||
_currentSettings = ret.Item2?.Item3;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderTryInheritMod, "Inherit Mod" );
|
||||
ImGui.Checkbox( "##inherit", ref _settingsInherit );
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Set##Inherit" ) )
|
||||
{
|
||||
_lastSettingsError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTryInheritMod )
|
||||
.InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderTrySetMod, "Set Enabled" );
|
||||
ImGui.Checkbox( "##enabled", ref _settingsEnabled );
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Set##Enabled" ) )
|
||||
{
|
||||
_lastSettingsError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetMod )
|
||||
.InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderTrySetModPriority, "Set Priority" );
|
||||
ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale );
|
||||
ImGui.DragInt( "##Priority", ref _settingsPriority );
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Set##Priority" ) )
|
||||
{
|
||||
_lastSettingsError = _pi
|
||||
.GetIpcSubscriber< string, string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModPriority )
|
||||
.InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderTrySetModSetting, "Set Setting(s)" );
|
||||
if( _availableSettings != null )
|
||||
{
|
||||
foreach( var (group, (list, type)) in _availableSettings )
|
||||
{
|
||||
using var id = ImRaii.PushId( group );
|
||||
var preview = list.Count > 0 ? list[ 0 ] : string.Empty;
|
||||
IList< string > current;
|
||||
if( _currentSettings != null && _currentSettings.TryGetValue( group, out current! ) && current.Count > 0 )
|
||||
{
|
||||
preview = current[ 0 ];
|
||||
}
|
||||
else
|
||||
{
|
||||
current = new List< string >();
|
||||
if( _currentSettings != null )
|
||||
{
|
||||
_currentSettings[ group ] = current;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale );
|
||||
using( var c = ImRaii.Combo( "##group", preview ) )
|
||||
{
|
||||
if( c )
|
||||
{
|
||||
foreach( var s in list )
|
||||
{
|
||||
var contained = current.Contains( s );
|
||||
if( ImGui.Checkbox( s, ref contained ) )
|
||||
{
|
||||
if( contained )
|
||||
{
|
||||
current.Add( s );
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Remove( s );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Set##setting" ) )
|
||||
{
|
||||
if( type == SelectType.Single )
|
||||
{
|
||||
_lastSettingsError = _pi
|
||||
.GetIpcSubscriber< string, string, string, string, string,
|
||||
PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSetting ).InvokeFunc( _settingsCollection,
|
||||
_settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[ 0 ] : string.Empty );
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastSettingsError = _pi
|
||||
.GetIpcSubscriber< string, string, string, string, IReadOnlyList< string >,
|
||||
PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSettings ).InvokeFunc( _settingsCollection,
|
||||
_settingsModDirectory, _settingsModName, group, current.ToArray() );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( group );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _tempCollectionName = string.Empty;
|
||||
private string _tempCharacterName = string.Empty;
|
||||
private bool _forceOverwrite = true;
|
||||
private string _tempModName = string.Empty;
|
||||
private PenumbraApiEc _lastTempError = PenumbraApiEc.Success;
|
||||
private string _lastCreatedCollectionName = string.Empty;
|
||||
private string _tempGamePath = "test/game/path.mtrl";
|
||||
private string _tempFilePath = "test/success.mtrl";
|
||||
private string _tempManipulation = string.Empty;
|
||||
|
||||
|
||||
private void DrawTemp()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode( "Temp IPC" );
|
||||
if( !_ )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.InputTextWithHint( "##tempCollection", "Collection Name...", ref _tempCollectionName, 128 );
|
||||
ImGui.InputTextWithHint( "##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32 );
|
||||
ImGui.InputTextWithHint( "##tempMod", "Temporary Mod Name...", ref _tempModName, 32 );
|
||||
ImGui.InputTextWithHint( "##tempGame", "Game Path...", ref _tempGamePath, 256 );
|
||||
ImGui.InputTextWithHint( "##tempFile", "File Path...", ref _tempFilePath, 256 );
|
||||
ImGui.InputTextWithHint( "##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256 );
|
||||
ImGui.Checkbox( "Force Character Collection Overwrite", ref _forceOverwrite );
|
||||
|
||||
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIntro( "Last Error", _lastTempError.ToString() );
|
||||
DrawIntro( "Last Created Collection", _lastCreatedCollectionName );
|
||||
DrawIntro( PenumbraIpc.LabelProviderCreateTemporaryCollection, "Create Temporary Collection" );
|
||||
if( ImGui.Button( "Create##Collection" ) )
|
||||
{
|
||||
( _lastTempError, _lastCreatedCollectionName ) =
|
||||
_pi.GetIpcSubscriber< string, string, bool, (PenumbraApiEc, string) >( PenumbraIpc.LabelProviderCreateTemporaryCollection )
|
||||
.InvokeFunc( _tempCollectionName, _tempCharacterName, _forceOverwrite );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryCollection, "Remove Temporary Collection from Character" );
|
||||
if( ImGui.Button( "Delete##Collection" ) )
|
||||
{
|
||||
_lastTempError = _pi.GetIpcSubscriber< string, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryCollection )
|
||||
.InvokeFunc( _tempCharacterName );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderAddTemporaryMod, "Add Temporary Mod to specific Collection" );
|
||||
if( ImGui.Button( "Add##Mod" ) )
|
||||
{
|
||||
_lastTempError = _pi
|
||||
.GetIpcSubscriber< string, string, Dictionary< string, string >, string, int, PenumbraApiEc >(
|
||||
PenumbraIpc.LabelProviderAddTemporaryMod )
|
||||
.InvokeFunc( _tempModName, _tempCollectionName,
|
||||
new Dictionary< string, string > { { _tempGamePath, _tempFilePath } },
|
||||
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderAddTemporaryModAll, "Add Temporary Mod to all Collections" );
|
||||
if( ImGui.Button( "Add##All" ) )
|
||||
{
|
||||
_lastTempError = _pi
|
||||
.GetIpcSubscriber< string, Dictionary< string, string >, string, int, PenumbraApiEc >(
|
||||
PenumbraIpc.LabelProviderAddTemporaryModAll )
|
||||
.InvokeFunc( _tempModName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } },
|
||||
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryMod, "Remove Temporary Mod from specific Collection" );
|
||||
if( ImGui.Button( "Remove##Mod" ) )
|
||||
{
|
||||
_lastTempError = _pi.GetIpcSubscriber< string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryMod )
|
||||
.InvokeFunc( _tempModName, _tempCollectionName, int.MaxValue );
|
||||
}
|
||||
|
||||
DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryModAll, "Remove Temporary Mod from all Collections" );
|
||||
if( ImGui.Button( "Remove##ModAll" ) )
|
||||
{
|
||||
_lastTempError = _pi.GetIpcSubscriber< string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryModAll )
|
||||
.InvokeFunc( _tempModName, int.MaxValue );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTempCollections()
|
||||
{
|
||||
using var collTree = ImRaii.TreeNode( "Collections" );
|
||||
if( !collTree )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##collTree", 4 );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (character, collection) in Penumbra.TempMods.Collections )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( character );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( collection.Name );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTempMods()
|
||||
{
|
||||
using var modTree = ImRaii.TreeNode( "Mods" );
|
||||
if( !modTree )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##modTree", 5 );
|
||||
|
||||
void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list )
|
||||
{
|
||||
foreach( var mod in list )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( mod.Name );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( mod.Priority.ToString() );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( collectionName );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( mod.Default.Files.Count.ToString() );
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
using var tt = ImRaii.Tooltip();
|
||||
foreach( var (path, file) in mod.Default.Files )
|
||||
{
|
||||
ImGui.TextUnformatted( $"{path} -> {file}" );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( mod.TotalManipulations.ToString() );
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
using var tt = ImRaii.Tooltip();
|
||||
foreach( var manip in mod.Default.Manipulations )
|
||||
{
|
||||
ImGui.TextUnformatted( manip.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( table )
|
||||
{
|
||||
PrintList( "All", Penumbra.TempMods.ModsForAllCollections );
|
||||
foreach( var (collection, list) in Penumbra.TempMods.Mods )
|
||||
{
|
||||
PrintList( collection.Name, list );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
Penumbra/Api/IpcTester/CollectionsIpcTester.cs
Normal file
189
Penumbra/Api/IpcTester/CollectionsIpcTester.cs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Data;
|
||||
using ImGuiClip = OtterGui.ImGuiClip;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
||||
{
|
||||
private int _objectIdx;
|
||||
private string _collectionIdString = string.Empty;
|
||||
private Guid? _collectionId;
|
||||
private bool _allowCreation = true;
|
||||
private bool _allowDeletion = true;
|
||||
private ApiCollectionType _type = ApiCollectionType.Yourself;
|
||||
|
||||
private Dictionary<Guid, string> _collections = [];
|
||||
private (string, ChangedItemType, uint)[] _changedItems = [];
|
||||
private PenumbraApiEc _returnCode = PenumbraApiEc.Success;
|
||||
private (Guid Id, string Name)? _oldCollection;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Collections");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName());
|
||||
ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0);
|
||||
ImGuiUtil.GuidInput("Collection Id##Collections", "Collection Identifier...", string.Empty, ref _collectionId, ref _collectionIdString);
|
||||
ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation);
|
||||
ImGui.SameLine();
|
||||
ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion);
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, 4, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro("Last Return Code", _returnCode.ToString());
|
||||
if (_oldCollection != null)
|
||||
ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString());
|
||||
|
||||
IpcTester.DrawIntro(GetCollectionsByIdentifier.Label, "Collection Identifier");
|
||||
var collectionList = new GetCollectionsByIdentifier(pi).Invoke(_collectionIdString);
|
||||
if (collectionList.Count == 0)
|
||||
{
|
||||
DrawCollection(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawCollection(collectionList[0]);
|
||||
foreach (var pair in collectionList.Skip(1))
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
DrawCollection(pair);
|
||||
}
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetCollection.Label, "Current Collection");
|
||||
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current));
|
||||
|
||||
IpcTester.DrawIntro(GetCollection.Label, "Default Collection");
|
||||
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Default));
|
||||
|
||||
IpcTester.DrawIntro(GetCollection.Label, "Interface Collection");
|
||||
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Interface));
|
||||
|
||||
IpcTester.DrawIntro(GetCollection.Label, "Special Collection");
|
||||
DrawCollection(new GetCollection(pi).Invoke(_type));
|
||||
|
||||
IpcTester.DrawIntro(GetCollections.Label, "Collections");
|
||||
DrawCollectionPopup();
|
||||
if (ImGui.Button("Get##Collections"))
|
||||
{
|
||||
_collections = new GetCollections(pi).Invoke();
|
||||
ImGui.OpenPopup("Collections");
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetCollectionForObject.Label, "Get Object Collection");
|
||||
var (valid, individual, effectiveCollection) = new GetCollectionForObject(pi).Invoke(_objectIdx);
|
||||
DrawCollection(effectiveCollection);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"({(valid ? "Valid" : "Invalid")} Object{(individual ? ", Individual Assignment)" : ")")}");
|
||||
|
||||
IpcTester.DrawIntro(SetCollection.Label, "Set Special Collection");
|
||||
if (ImGui.Button("Set##SpecialCollection"))
|
||||
(_returnCode, _oldCollection) =
|
||||
new SetCollection(pi).Invoke(_type, _collectionId.GetValueOrDefault(Guid.Empty), _allowCreation, _allowDeletion);
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGui.Button("Remove##SpecialCollection"))
|
||||
(_returnCode, _oldCollection) = new SetCollection(pi).Invoke(_type, null, _allowCreation, _allowDeletion);
|
||||
|
||||
IpcTester.DrawIntro(SetCollectionForObject.Label, "Set Object Collection");
|
||||
if (ImGui.Button("Set##ObjectCollection"))
|
||||
(_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, _collectionId.GetValueOrDefault(Guid.Empty),
|
||||
_allowCreation, _allowDeletion);
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGui.Button("Remove##ObjectCollection"))
|
||||
(_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, null, _allowCreation, _allowDeletion);
|
||||
|
||||
IpcTester.DrawIntro(GetChangedItemsForCollection.Label, "Changed Item List");
|
||||
DrawChangedItemPopup();
|
||||
if (ImGui.Button("Get##ChangedItems"))
|
||||
{
|
||||
var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
|
||||
_changedItems = items.Select(kvp =>
|
||||
{
|
||||
var (type, id) = kvp.Value.ToApiObject();
|
||||
return (kvp.Key, type, id);
|
||||
}).ToArray();
|
||||
ImGui.OpenPopup("Changed Item List");
|
||||
}
|
||||
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
|
||||
if (ImGui.Button("Redraw##ObjectCollection"))
|
||||
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
|
||||
|
||||
}
|
||||
|
||||
private void DrawChangedItemPopup()
|
||||
{
|
||||
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
|
||||
using var p = ImRaii.Popup("Changed Item List");
|
||||
if (!p)
|
||||
return;
|
||||
|
||||
using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
|
||||
{
|
||||
if (table)
|
||||
ImGuiClip.ClippedDraw(_changedItems, t =>
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn(t.Item1);
|
||||
ImGuiUtil.DrawTableColumn(t.Item2.ToString());
|
||||
ImGuiUtil.DrawTableColumn(t.Item3.ToString());
|
||||
}, ImGui.GetTextLineHeightWithSpacing());
|
||||
}
|
||||
|
||||
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
private void DrawCollectionPopup()
|
||||
{
|
||||
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
|
||||
using var p = ImRaii.Popup("Collections");
|
||||
if (!p)
|
||||
return;
|
||||
|
||||
using (var t = ImRaii.Table("collections", 2, ImGuiTableFlags.SizingFixedFit))
|
||||
{
|
||||
if (t)
|
||||
foreach (var collection in _collections)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
DrawCollection((collection.Key, collection.Value));
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
private static void DrawCollection((Guid Id, string Name)? collection)
|
||||
{
|
||||
if (collection == null)
|
||||
{
|
||||
ImGui.TextUnformatted("<Unassigned>");
|
||||
ImGui.TableNextColumn();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted(collection.Value.Name);
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
ImGuiUtil.CopyOnClickSelectable(collection.Value.Id.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Penumbra/Api/IpcTester/EditingIpcTester.cs
Normal file
70
Penumbra/Api/IpcTester/EditingIpcTester.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class EditingIpcTester(IDalamudPluginInterface pi) : IUiService
|
||||
{
|
||||
private string _inputPath = string.Empty;
|
||||
private string _inputPath2 = string.Empty;
|
||||
private string _outputPath = string.Empty;
|
||||
private string _outputPath2 = string.Empty;
|
||||
|
||||
private TextureType _typeSelector;
|
||||
private bool _mipMaps = true;
|
||||
|
||||
private Task? _task1;
|
||||
private Task? _task2;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Editing");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256);
|
||||
ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256);
|
||||
ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256);
|
||||
ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256);
|
||||
TypeCombo();
|
||||
ImGui.Checkbox("Add MipMaps", ref _mipMaps);
|
||||
|
||||
using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 1");
|
||||
if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false }))
|
||||
_task1 = new ConvertTextureFile(pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString());
|
||||
if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted)
|
||||
ImGui.SetTooltip(_task1.Exception?.ToString());
|
||||
|
||||
IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 2");
|
||||
if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false }))
|
||||
_task2 = new ConvertTextureFile(pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString());
|
||||
if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted)
|
||||
ImGui.SetTooltip(_task2.Exception?.ToString());
|
||||
}
|
||||
|
||||
private void TypeCombo()
|
||||
{
|
||||
using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString());
|
||||
if (!combo)
|
||||
return;
|
||||
|
||||
foreach (var value in Enum.GetValues<TextureType>())
|
||||
{
|
||||
if (ImGui.Selectable(value.ToString(), _typeSelector == value))
|
||||
_typeSelector = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
139
Penumbra/Api/IpcTester/GameStateIpcTester.cs
Normal file
139
Penumbra/Api/IpcTester/GameStateIpcTester.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class GameStateIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
public readonly EventSubscriber<nint, Guid, nint, nint, nint> CharacterBaseCreating;
|
||||
public readonly EventSubscriber<nint, Guid, nint> CharacterBaseCreated;
|
||||
public readonly EventSubscriber<nint, string, string> GameObjectResourcePathResolved;
|
||||
|
||||
private string _lastCreatedGameObjectName = string.Empty;
|
||||
private nint _lastCreatedDrawObject = nint.Zero;
|
||||
private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
|
||||
private string _lastResolvedGamePath = string.Empty;
|
||||
private string _lastResolvedFullPath = string.Empty;
|
||||
private string _lastResolvedObject = string.Empty;
|
||||
private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue;
|
||||
private string _currentDrawObjectString = string.Empty;
|
||||
private nint _currentDrawObject = nint.Zero;
|
||||
private int _currentCutsceneActor;
|
||||
private int _currentCutsceneParent;
|
||||
private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success;
|
||||
|
||||
public GameStateIpcTester(IDalamudPluginInterface pi)
|
||||
{
|
||||
_pi = pi;
|
||||
CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated);
|
||||
CharacterBaseCreated = IpcSubscribers.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2);
|
||||
GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath);
|
||||
CharacterBaseCreating.Disable();
|
||||
CharacterBaseCreated.Disable();
|
||||
GameObjectResourcePathResolved.Disable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CharacterBaseCreating.Dispose();
|
||||
CharacterBaseCreated.Dispose();
|
||||
GameObjectResourcePathResolved.Dispose();
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Game State");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16,
|
||||
ImGuiInputTextFlags.CharsHexadecimal))
|
||||
_currentDrawObject = nint.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
|
||||
out var tmp)
|
||||
? tmp
|
||||
: nint.Zero;
|
||||
|
||||
ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0);
|
||||
ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0);
|
||||
if (_cutsceneError is not PenumbraApiEc.Success)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted("Invalid Argument on last Call");
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(GetDrawObjectInfo.Label, "Draw Object Info");
|
||||
if (_currentDrawObject == nint.Zero)
|
||||
{
|
||||
ImGui.TextUnformatted("Invalid");
|
||||
}
|
||||
else
|
||||
{
|
||||
var (ptr, (collectionId, collectionName)) = new GetDrawObjectInfo(_pi).Invoke(_currentDrawObject);
|
||||
ImGui.TextUnformatted(ptr == nint.Zero ? $"No Actor Associated, {collectionName}" : $"{ptr:X}, {collectionName}");
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
ImGui.TextUnformatted(collectionId.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetCutsceneParentIndex.Label, "Cutscene Parent");
|
||||
ImGui.TextUnformatted(new GetCutsceneParentIndex(_pi).Invoke(_currentCutsceneActor).ToString());
|
||||
|
||||
IpcTester.DrawIntro(SetCutsceneParentIndex.Label, "Cutscene Parent");
|
||||
if (ImGui.Button("Set Parent"))
|
||||
_cutsceneError = new SetCutsceneParentIndex(_pi)
|
||||
.Invoke(_currentCutsceneActor, _currentCutsceneParent);
|
||||
|
||||
IpcTester.DrawIntro(CreatingCharacterBase.Label, "Last Drawobject created");
|
||||
if (_lastCreatedGameObjectTime < DateTimeOffset.Now)
|
||||
ImGui.TextUnformatted(_lastCreatedDrawObject != nint.Zero
|
||||
? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"
|
||||
: $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}");
|
||||
|
||||
IpcTester.DrawIntro(IpcSubscribers.GameObjectResourcePathResolved.Label, "Last GamePath resolved");
|
||||
if (_lastResolvedGamePathTime < DateTimeOffset.Now)
|
||||
ImGui.TextUnformatted(
|
||||
$"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}");
|
||||
}
|
||||
|
||||
private void UpdateLastCreated(nint gameObject, Guid _, nint _2, nint _3, nint _4)
|
||||
{
|
||||
_lastCreatedGameObjectName = GetObjectName(gameObject);
|
||||
_lastCreatedGameObjectTime = DateTimeOffset.Now;
|
||||
_lastCreatedDrawObject = nint.Zero;
|
||||
}
|
||||
|
||||
private void UpdateLastCreated2(nint gameObject, Guid _, nint drawObject)
|
||||
{
|
||||
_lastCreatedGameObjectName = GetObjectName(gameObject);
|
||||
_lastCreatedGameObjectTime = DateTimeOffset.Now;
|
||||
_lastCreatedDrawObject = drawObject;
|
||||
}
|
||||
|
||||
private void UpdateGameObjectResourcePath(nint gameObject, string gamePath, string fullPath)
|
||||
{
|
||||
_lastResolvedObject = GetObjectName(gameObject);
|
||||
_lastResolvedGamePath = gamePath;
|
||||
_lastResolvedFullPath = fullPath;
|
||||
_lastResolvedGamePathTime = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
private static unsafe string GetObjectName(nint gameObject)
|
||||
{
|
||||
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject;
|
||||
return obj != null && obj->Name[0] != 0 ? new ByteString(obj->Name).ToString() : "Unknown";
|
||||
}
|
||||
}
|
||||
133
Penumbra/Api/IpcTester/IpcTester.cs
Normal file
133
Penumbra/Api/IpcTester/IpcTester.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class IpcTester(
|
||||
IpcProviders ipcProviders,
|
||||
IPenumbraApi api,
|
||||
PluginStateIpcTester pluginStateIpcTester,
|
||||
UiIpcTester uiIpcTester,
|
||||
RedrawingIpcTester redrawingIpcTester,
|
||||
GameStateIpcTester gameStateIpcTester,
|
||||
ResolveIpcTester resolveIpcTester,
|
||||
CollectionsIpcTester collectionsIpcTester,
|
||||
MetaIpcTester metaIpcTester,
|
||||
ModsIpcTester modsIpcTester,
|
||||
ModSettingsIpcTester modSettingsIpcTester,
|
||||
EditingIpcTester editingIpcTester,
|
||||
TemporaryIpcTester temporaryIpcTester,
|
||||
ResourceTreeIpcTester resourceTreeIpcTester,
|
||||
IFramework framework) : IUiService
|
||||
{
|
||||
private readonly IpcProviders _ipcProviders = ipcProviders;
|
||||
private DateTime _lastUpdate;
|
||||
private bool _subscribed = false;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
try
|
||||
{
|
||||
_lastUpdate = framework.LastUpdateUTC.AddSeconds(1);
|
||||
Subscribe();
|
||||
|
||||
ImGui.TextUnformatted($"API Version: {api.ApiVersion.Breaking}.{api.ApiVersion.Feature:D4}");
|
||||
collectionsIpcTester.Draw();
|
||||
editingIpcTester.Draw();
|
||||
gameStateIpcTester.Draw();
|
||||
metaIpcTester.Draw();
|
||||
modSettingsIpcTester.Draw();
|
||||
modsIpcTester.Draw();
|
||||
pluginStateIpcTester.Draw();
|
||||
redrawingIpcTester.Draw();
|
||||
resolveIpcTester.Draw();
|
||||
resourceTreeIpcTester.Draw();
|
||||
uiIpcTester.Draw();
|
||||
temporaryIpcTester.Draw();
|
||||
temporaryIpcTester.DrawCollections();
|
||||
temporaryIpcTester.DrawMods();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Error during IPC Tests:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void DrawIntro(string label, string info)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(label);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(info);
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
if (_subscribed)
|
||||
return;
|
||||
|
||||
Penumbra.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester.");
|
||||
gameStateIpcTester.GameObjectResourcePathResolved.Enable();
|
||||
gameStateIpcTester.CharacterBaseCreated.Enable();
|
||||
gameStateIpcTester.CharacterBaseCreating.Enable();
|
||||
modSettingsIpcTester.SettingChanged.Enable();
|
||||
modsIpcTester.DeleteSubscriber.Enable();
|
||||
modsIpcTester.AddSubscriber.Enable();
|
||||
modsIpcTester.MoveSubscriber.Enable();
|
||||
pluginStateIpcTester.ModDirectoryChanged.Enable();
|
||||
pluginStateIpcTester.Initialized.Enable();
|
||||
pluginStateIpcTester.Disposed.Enable();
|
||||
pluginStateIpcTester.EnabledChange.Enable();
|
||||
redrawingIpcTester.Redrawn.Enable();
|
||||
uiIpcTester.PreSettingsTabBar.Enable();
|
||||
uiIpcTester.PreSettingsPanel.Enable();
|
||||
uiIpcTester.PostEnabled.Enable();
|
||||
uiIpcTester.PostSettingsPanelDraw.Enable();
|
||||
uiIpcTester.ChangedItemTooltip.Enable();
|
||||
uiIpcTester.ChangedItemClicked.Enable();
|
||||
|
||||
framework.Update += CheckUnsubscribe;
|
||||
_subscribed = true;
|
||||
}
|
||||
|
||||
private void CheckUnsubscribe(IFramework framework1)
|
||||
{
|
||||
if (_lastUpdate > framework.LastUpdateUTC)
|
||||
return;
|
||||
|
||||
Unsubscribe();
|
||||
framework.Update -= CheckUnsubscribe;
|
||||
}
|
||||
|
||||
private void Unsubscribe()
|
||||
{
|
||||
if (!_subscribed)
|
||||
return;
|
||||
|
||||
Penumbra.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester.");
|
||||
_subscribed = false;
|
||||
gameStateIpcTester.GameObjectResourcePathResolved.Disable();
|
||||
gameStateIpcTester.CharacterBaseCreated.Disable();
|
||||
gameStateIpcTester.CharacterBaseCreating.Disable();
|
||||
modSettingsIpcTester.SettingChanged.Disable();
|
||||
modsIpcTester.DeleteSubscriber.Disable();
|
||||
modsIpcTester.AddSubscriber.Disable();
|
||||
modsIpcTester.MoveSubscriber.Disable();
|
||||
pluginStateIpcTester.ModDirectoryChanged.Disable();
|
||||
pluginStateIpcTester.Initialized.Disable();
|
||||
pluginStateIpcTester.Disposed.Disable();
|
||||
pluginStateIpcTester.EnabledChange.Disable();
|
||||
redrawingIpcTester.Redrawn.Disable();
|
||||
uiIpcTester.PreSettingsTabBar.Disable();
|
||||
uiIpcTester.PreSettingsPanel.Disable();
|
||||
uiIpcTester.PostEnabled.Disable();
|
||||
uiIpcTester.PostSettingsPanelDraw.Disable();
|
||||
uiIpcTester.ChangedItemTooltip.Disable();
|
||||
uiIpcTester.ChangedItemClicked.Disable();
|
||||
}
|
||||
}
|
||||
52
Penumbra/Api/IpcTester/MetaIpcTester.cs
Normal file
52
Penumbra/Api/IpcTester/MetaIpcTester.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService
|
||||
{
|
||||
private int _gameObjectIndex;
|
||||
private string _metaBase64 = string.Empty;
|
||||
private MetaDictionary _metaDict = new();
|
||||
private byte _parsedVersion = byte.MaxValue;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Meta");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0);
|
||||
if (ImUtf8.InputText("##metaText"u8, ref _metaBase64, "Base64 Metadata..."u8))
|
||||
if (!MetaApi.ConvertManips(_metaBase64, out _metaDict!, out _parsedVersion))
|
||||
_metaDict ??= new MetaDictionary();
|
||||
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(GetPlayerMetaManipulations.Label, "Player Meta Manipulations");
|
||||
if (ImGui.Button("Copy to Clipboard##Player"))
|
||||
{
|
||||
var base64 = new GetPlayerMetaManipulations(pi).Invoke();
|
||||
ImGui.SetClipboardText(base64);
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetMetaManipulations.Label, "Game Object Manipulations");
|
||||
if (ImGui.Button("Copy to Clipboard##GameObject"))
|
||||
{
|
||||
var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex);
|
||||
ImGui.SetClipboardText(base64);
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(string.Empty, "Parsed Data");
|
||||
ImUtf8.Text($"Version: {_parsedVersion}, Count: {_metaDict.Count}");
|
||||
}
|
||||
}
|
||||
224
Penumbra/Api/IpcTester/ModSettingsIpcTester.cs
Normal file
224
Penumbra/Api/IpcTester/ModSettingsIpcTester.cs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.UI;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class ModSettingsIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
public readonly EventSubscriber<ModSettingChange, Guid, string, bool> SettingChanged;
|
||||
|
||||
private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
|
||||
private ModSettingChange _lastSettingChangeType;
|
||||
private Guid _lastSettingChangeCollection = Guid.Empty;
|
||||
private string _lastSettingChangeMod = string.Empty;
|
||||
private bool _lastSettingChangeInherited;
|
||||
private DateTimeOffset _lastSettingChange;
|
||||
|
||||
private string _settingsModDirectory = string.Empty;
|
||||
private string _settingsModName = string.Empty;
|
||||
private Guid? _settingsCollection;
|
||||
private string _settingsCollectionName = string.Empty;
|
||||
private bool _settingsIgnoreInheritance;
|
||||
private bool _settingsIgnoreTemporary;
|
||||
private int _settingsKey;
|
||||
private bool _settingsInherit;
|
||||
private bool _settingsTemporary;
|
||||
private bool _settingsEnabled;
|
||||
private int _settingsPriority;
|
||||
private IReadOnlyDictionary<string, (string[], GroupType)>? _availableSettings;
|
||||
private Dictionary<string, List<string>>? _currentSettings;
|
||||
private Dictionary<string, (bool, int, Dictionary<string, List<string>>, bool, bool)>? _allSettings;
|
||||
|
||||
public ModSettingsIpcTester(IDalamudPluginInterface pi)
|
||||
{
|
||||
_pi = pi;
|
||||
SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting);
|
||||
SettingChanged.Disable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SettingChanged.Dispose();
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Mod Settings");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100);
|
||||
ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100);
|
||||
ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName);
|
||||
ImUtf8.Checkbox("Ignore Inheritance"u8, ref _settingsIgnoreInheritance);
|
||||
ImUtf8.Checkbox("Ignore Temporary"u8, ref _settingsIgnoreTemporary);
|
||||
ImUtf8.InputScalar("Key"u8, ref _settingsKey);
|
||||
var collection = _settingsCollection.GetValueOrDefault(Guid.Empty);
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro("Last Error", _lastSettingsError.ToString());
|
||||
|
||||
IpcTester.DrawIntro(ModSettingChanged.Label, "Last Mod Setting Changed");
|
||||
ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0
|
||||
? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}"
|
||||
: "None");
|
||||
|
||||
IpcTester.DrawIntro(GetAvailableModSettings.Label, "Get Available Settings");
|
||||
if (ImGui.Button("Get##Available"))
|
||||
{
|
||||
_availableSettings = new GetAvailableModSettings(_pi).Invoke(_settingsModDirectory, _settingsModName);
|
||||
_lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success;
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetCurrentModSettings.Label, "Get Current Settings");
|
||||
if (ImGui.Button("Get##Current"))
|
||||
{
|
||||
var ret = new GetCurrentModSettings(_pi)
|
||||
.Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance);
|
||||
_lastSettingsError = ret.Item1;
|
||||
if (ret.Item1 == PenumbraApiEc.Success)
|
||||
{
|
||||
_settingsEnabled = ret.Item2?.Item1 ?? false;
|
||||
_settingsInherit = ret.Item2?.Item4 ?? true;
|
||||
_settingsTemporary = false;
|
||||
_settingsPriority = ret.Item2?.Item2 ?? 0;
|
||||
_currentSettings = ret.Item2?.Item3;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetCurrentModSettingsWithTemp.Label, "Get Current Settings With Temp");
|
||||
if (ImGui.Button("Get##CurrentTemp"))
|
||||
{
|
||||
var ret = new GetCurrentModSettingsWithTemp(_pi)
|
||||
.Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey);
|
||||
_lastSettingsError = ret.Item1;
|
||||
if (ret.Item1 == PenumbraApiEc.Success)
|
||||
{
|
||||
_settingsEnabled = ret.Item2?.Item1 ?? false;
|
||||
_settingsInherit = ret.Item2?.Item4 ?? true;
|
||||
_settingsTemporary = ret.Item2?.Item5 ?? false;
|
||||
_settingsPriority = ret.Item2?.Item2 ?? 0;
|
||||
_currentSettings = ret.Item2?.Item3;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetAllModSettings.Label, "Get All Mod Settings");
|
||||
if (ImGui.Button("Get##All"))
|
||||
{
|
||||
var ret = new GetAllModSettings(_pi).Invoke(collection, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey);
|
||||
_lastSettingsError = ret.Item1;
|
||||
_allSettings = ret.Item2;
|
||||
}
|
||||
|
||||
if (_allSettings != null)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImUtf8.Text($"{_allSettings.Count} Mods");
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod");
|
||||
ImGui.Checkbox("##inherit", ref _settingsInherit);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Set##Inherit"))
|
||||
_lastSettingsError = new TryInheritMod(_pi)
|
||||
.Invoke(collection, _settingsModDirectory, _settingsInherit, _settingsModName);
|
||||
|
||||
IpcTester.DrawIntro(TrySetMod.Label, "Set Enabled");
|
||||
ImGui.Checkbox("##enabled", ref _settingsEnabled);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Set##Enabled"))
|
||||
_lastSettingsError = new TrySetMod(_pi)
|
||||
.Invoke(collection, _settingsModDirectory, _settingsEnabled, _settingsModName);
|
||||
|
||||
IpcTester.DrawIntro(TrySetModPriority.Label, "Set Priority");
|
||||
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
|
||||
ImGui.DragInt("##Priority", ref _settingsPriority);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Set##Priority"))
|
||||
_lastSettingsError = new TrySetModPriority(_pi)
|
||||
.Invoke(collection, _settingsModDirectory, _settingsPriority, _settingsModName);
|
||||
|
||||
IpcTester.DrawIntro(CopyModSettings.Label, "Copy Mod Settings");
|
||||
if (ImGui.Button("Copy Settings"))
|
||||
_lastSettingsError = new CopyModSettings(_pi)
|
||||
.Invoke(_settingsCollection, _settingsModDirectory, _settingsModName);
|
||||
|
||||
ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection.");
|
||||
|
||||
IpcTester.DrawIntro(TrySetModSetting.Label, "Set Setting(s)");
|
||||
if (_availableSettings == null)
|
||||
return;
|
||||
|
||||
foreach (var (group, (list, type)) in _availableSettings)
|
||||
{
|
||||
using var id = ImRaii.PushId(group);
|
||||
var preview = list.Length > 0 ? list[0] : string.Empty;
|
||||
if (_currentSettings != null && _currentSettings.TryGetValue(group, out var current) && current.Count > 0)
|
||||
{
|
||||
preview = current[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
current = [];
|
||||
if (_currentSettings != null)
|
||||
_currentSettings[group] = current;
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
|
||||
using (var c = ImRaii.Combo("##group", preview))
|
||||
{
|
||||
if (c)
|
||||
foreach (var s in list)
|
||||
{
|
||||
var contained = current.Contains(s);
|
||||
if (ImGui.Checkbox(s, ref contained))
|
||||
{
|
||||
if (contained)
|
||||
current.Add(s);
|
||||
else
|
||||
current.Remove(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Set##setting"))
|
||||
_lastSettingsError = type == GroupType.Single
|
||||
? new TrySetModSetting(_pi).Invoke(collection, _settingsModDirectory, group, current.Count > 0 ? current[0] : string.Empty,
|
||||
_settingsModName)
|
||||
: new TrySetModSettings(_pi).Invoke(collection, _settingsModDirectory, group, current.ToArray(), _settingsModName);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(group);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLastModSetting(ModSettingChange type, Guid collection, string mod, bool inherited)
|
||||
{
|
||||
_lastSettingChangeType = type;
|
||||
_lastSettingChangeCollection = collection;
|
||||
_lastSettingChangeMod = mod;
|
||||
_lastSettingChangeInherited = inherited;
|
||||
_lastSettingChange = DateTimeOffset.Now;
|
||||
}
|
||||
}
|
||||
184
Penumbra/Api/IpcTester/ModsIpcTester.cs
Normal file
184
Penumbra/Api/IpcTester/ModsIpcTester.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class ModsIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
|
||||
private string _modDirectory = string.Empty;
|
||||
private string _modName = string.Empty;
|
||||
private string _pathInput = string.Empty;
|
||||
private string _newInstallPath = string.Empty;
|
||||
private PenumbraApiEc _lastReloadEc;
|
||||
private PenumbraApiEc _lastAddEc;
|
||||
private PenumbraApiEc _lastDeleteEc;
|
||||
private PenumbraApiEc _lastSetPathEc;
|
||||
private PenumbraApiEc _lastInstallEc;
|
||||
private Dictionary<string, string> _mods = [];
|
||||
private Dictionary<string, object?> _changedItems = [];
|
||||
|
||||
public readonly EventSubscriber<string> DeleteSubscriber;
|
||||
public readonly EventSubscriber<string> AddSubscriber;
|
||||
public readonly EventSubscriber<string, string> MoveSubscriber;
|
||||
|
||||
private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch;
|
||||
private string _lastDeletedMod = string.Empty;
|
||||
private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch;
|
||||
private string _lastAddedMod = string.Empty;
|
||||
private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch;
|
||||
private string _lastMovedModFrom = string.Empty;
|
||||
private string _lastMovedModTo = string.Empty;
|
||||
|
||||
public ModsIpcTester(IDalamudPluginInterface pi)
|
||||
{
|
||||
_pi = pi;
|
||||
DeleteSubscriber = ModDeleted.Subscriber(pi, s =>
|
||||
{
|
||||
_lastDeletedModTime = DateTimeOffset.UtcNow;
|
||||
_lastDeletedMod = s;
|
||||
});
|
||||
AddSubscriber = ModAdded.Subscriber(pi, s =>
|
||||
{
|
||||
_lastAddedModTime = DateTimeOffset.UtcNow;
|
||||
_lastAddedMod = s;
|
||||
});
|
||||
MoveSubscriber = ModMoved.Subscriber(pi, (s1, s2) =>
|
||||
{
|
||||
_lastMovedModTime = DateTimeOffset.UtcNow;
|
||||
_lastMovedModFrom = s1;
|
||||
_lastMovedModTo = s2;
|
||||
});
|
||||
DeleteSubscriber.Disable();
|
||||
AddSubscriber.Disable();
|
||||
MoveSubscriber.Disable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DeleteSubscriber.Dispose();
|
||||
DeleteSubscriber.Disable();
|
||||
AddSubscriber.Dispose();
|
||||
AddSubscriber.Disable();
|
||||
MoveSubscriber.Dispose();
|
||||
MoveSubscriber.Disable();
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Mods");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100);
|
||||
ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100);
|
||||
ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100);
|
||||
ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100);
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(GetModList.Label, "Mods");
|
||||
DrawModsPopup();
|
||||
if (ImGui.Button("Get##Mods"))
|
||||
{
|
||||
_mods = new GetModList(_pi).Invoke();
|
||||
ImGui.OpenPopup("Mods");
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(ReloadMod.Label, "Reload Mod");
|
||||
if (ImGui.Button("Reload"))
|
||||
_lastReloadEc = new ReloadMod(_pi).Invoke(_modDirectory, _modName);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_lastReloadEc.ToString());
|
||||
|
||||
IpcTester.DrawIntro(InstallMod.Label, "Install Mod");
|
||||
if (ImGui.Button("Install"))
|
||||
_lastInstallEc = new InstallMod(_pi).Invoke(_newInstallPath);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_lastInstallEc.ToString());
|
||||
|
||||
IpcTester.DrawIntro(AddMod.Label, "Add Mod");
|
||||
if (ImGui.Button("Add"))
|
||||
_lastAddEc = new AddMod(_pi).Invoke(_modDirectory);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_lastAddEc.ToString());
|
||||
|
||||
IpcTester.DrawIntro(DeleteMod.Label, "Delete Mod");
|
||||
if (ImGui.Button("Delete"))
|
||||
_lastDeleteEc = new DeleteMod(_pi).Invoke(_modDirectory, _modName);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_lastDeleteEc.ToString());
|
||||
|
||||
IpcTester.DrawIntro(GetChangedItems.Label, "Get Changed Items");
|
||||
DrawChangedItemsPopup();
|
||||
if (ImUtf8.Button("Get##ChangedItems"u8))
|
||||
{
|
||||
_changedItems = new GetChangedItems(_pi).Invoke(_modDirectory, _modName);
|
||||
ImUtf8.OpenPopup("ChangedItems"u8);
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetModPath.Label, "Current Path");
|
||||
var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName);
|
||||
ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]");
|
||||
|
||||
IpcTester.DrawIntro(SetModPath.Label, "Set Path");
|
||||
if (ImGui.Button("Set"))
|
||||
_lastSetPathEc = new SetModPath(_pi).Invoke(_modDirectory, _pathInput, _modName);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_lastSetPathEc.ToString());
|
||||
|
||||
IpcTester.DrawIntro(ModDeleted.Label, "Last Mod Deleted");
|
||||
if (_lastDeletedModTime > DateTimeOffset.UnixEpoch)
|
||||
ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}");
|
||||
|
||||
IpcTester.DrawIntro(ModAdded.Label, "Last Mod Added");
|
||||
if (_lastAddedModTime > DateTimeOffset.UnixEpoch)
|
||||
ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}");
|
||||
|
||||
IpcTester.DrawIntro(ModMoved.Label, "Last Mod Moved");
|
||||
if (_lastMovedModTime > DateTimeOffset.UnixEpoch)
|
||||
ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}");
|
||||
}
|
||||
|
||||
private void DrawModsPopup()
|
||||
{
|
||||
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
|
||||
using var p = ImRaii.Popup("Mods");
|
||||
if (!p)
|
||||
return;
|
||||
|
||||
foreach (var (modDir, modName) in _mods)
|
||||
ImGui.TextUnformatted($"{modDir}: {modName}");
|
||||
|
||||
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
private void DrawChangedItemsPopup()
|
||||
{
|
||||
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
|
||||
using var p = ImUtf8.Popup("ChangedItems"u8);
|
||||
if (!p)
|
||||
return;
|
||||
|
||||
foreach (var (name, data) in _changedItems)
|
||||
ImUtf8.Text($"{name}: {data}");
|
||||
|
||||
if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused())
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
147
Penumbra/Api/IpcTester/PluginStateIpcTester.cs
Normal file
147
Penumbra/Api/IpcTester/PluginStateIpcTester.cs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class PluginStateIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
|
||||
public readonly EventSubscriber Initialized;
|
||||
public readonly EventSubscriber Disposed;
|
||||
public readonly EventSubscriber<bool> EnabledChange;
|
||||
|
||||
private string _currentConfiguration = string.Empty;
|
||||
private string _lastModDirectory = string.Empty;
|
||||
private bool _lastModDirectoryValid;
|
||||
private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue;
|
||||
|
||||
private readonly List<DateTimeOffset> _initializedList = [];
|
||||
private readonly List<DateTimeOffset> _disposedList = [];
|
||||
|
||||
private string _requiredFeatureString = string.Empty;
|
||||
private string[] _requiredFeatures = [];
|
||||
|
||||
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
|
||||
private bool? _lastEnabledValue;
|
||||
|
||||
public PluginStateIpcTester(IDalamudPluginInterface pi)
|
||||
{
|
||||
_pi = pi;
|
||||
ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged);
|
||||
Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized);
|
||||
Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed);
|
||||
EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled);
|
||||
ModDirectoryChanged.Disable();
|
||||
EnabledChange.Disable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ModDirectoryChanged.Dispose();
|
||||
Initialized.Dispose();
|
||||
Disposed.Dispose();
|
||||
EnabledChange.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Plugin State");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
if (ImUtf8.InputText("Required Features"u8, ref _requiredFeatureString))
|
||||
_requiredFeatures = _requiredFeatureString.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
DrawList(IpcSubscribers.Initialized.Label, "Last Initialized", _initializedList);
|
||||
DrawList(IpcSubscribers.Disposed.Label, "Last Disposed", _disposedList);
|
||||
|
||||
IpcTester.DrawIntro(ApiVersion.Label, "Current Version");
|
||||
var (breaking, features) = new ApiVersion(_pi).Invoke();
|
||||
ImGui.TextUnformatted($"{breaking}.{features:D4}");
|
||||
|
||||
IpcTester.DrawIntro(GetEnabledState.Label, "Current State");
|
||||
ImGui.TextUnformatted($"{new GetEnabledState(_pi).Invoke()}");
|
||||
|
||||
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
|
||||
ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
|
||||
|
||||
IpcTester.DrawIntro(SupportedFeatures.Label, "Supported Features");
|
||||
ImUtf8.Text(string.Join(", ", new SupportedFeatures(_pi).Invoke()));
|
||||
|
||||
IpcTester.DrawIntro(CheckSupportedFeatures.Label, "Missing Features");
|
||||
ImUtf8.Text(string.Join(", ", new CheckSupportedFeatures(_pi).Invoke(_requiredFeatures)));
|
||||
|
||||
DrawConfigPopup();
|
||||
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
|
||||
if (ImGui.Button("Get"))
|
||||
{
|
||||
_currentConfiguration = new GetConfiguration(_pi).Invoke();
|
||||
ImGui.OpenPopup("Config Popup");
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetModDirectory.Label, "Current Mod Directory");
|
||||
ImGui.TextUnformatted(new GetModDirectory(_pi).Invoke());
|
||||
|
||||
IpcTester.DrawIntro(IpcSubscribers.ModDirectoryChanged.Label, "Last Mod Directory Change");
|
||||
ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue
|
||||
? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}"
|
||||
: "None");
|
||||
|
||||
void DrawList(string label, string text, List<DateTimeOffset> list)
|
||||
{
|
||||
IpcTester.DrawIntro(label, text);
|
||||
if (list.Count == 0)
|
||||
{
|
||||
ImGui.TextUnformatted("Never");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture));
|
||||
if (list.Count > 1 && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(string.Join("\n",
|
||||
list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawConfigPopup()
|
||||
{
|
||||
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
|
||||
using var popup = ImRaii.Popup("Config Popup");
|
||||
if (!popup)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
ImGuiUtil.TextWrapped(_currentConfiguration);
|
||||
}
|
||||
|
||||
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
private void UpdateModDirectoryChanged(string path, bool valid)
|
||||
=> (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now);
|
||||
|
||||
private void AddInitialized()
|
||||
=> _initializedList.Add(DateTimeOffset.UtcNow);
|
||||
|
||||
private void AddDisposed()
|
||||
=> _disposedList.Add(DateTimeOffset.UtcNow);
|
||||
|
||||
private void SetLastEnabled(bool val)
|
||||
=> (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val);
|
||||
}
|
||||
73
Penumbra/Api/IpcTester/RedrawingIpcTester.cs
Normal file
73
Penumbra/Api/IpcTester/RedrawingIpcTester.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.UI;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class RedrawingIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly ObjectManager _objects;
|
||||
public readonly EventSubscriber<nint, int> Redrawn;
|
||||
|
||||
private int _redrawIndex;
|
||||
private string _lastRedrawnString = "None";
|
||||
|
||||
public RedrawingIpcTester(IDalamudPluginInterface pi, ObjectManager objects)
|
||||
{
|
||||
_pi = pi;
|
||||
_objects = objects;
|
||||
Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn);
|
||||
Redrawn.Disable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Redrawn.Dispose();
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Redrawing");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(RedrawObject.Label, "Redraw by Index");
|
||||
var tmp = _redrawIndex;
|
||||
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
|
||||
if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount))
|
||||
_redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Redraw##Index"))
|
||||
new RedrawObject(_pi).Invoke(_redrawIndex);
|
||||
|
||||
IpcTester.DrawIntro(RedrawAll.Label, "Redraw All");
|
||||
if (ImGui.Button("Redraw##All"))
|
||||
new RedrawAll(_pi).Invoke();
|
||||
|
||||
IpcTester.DrawIntro(GameObjectRedrawn.Label, "Last Redrawn Object:");
|
||||
ImGui.TextUnformatted(_lastRedrawnString);
|
||||
}
|
||||
|
||||
private void SetLastRedrawn(nint address, int index)
|
||||
{
|
||||
if (index < 0
|
||||
|| index > _objects.TotalCount
|
||||
|| address == nint.Zero
|
||||
|| _objects[index].Address != address)
|
||||
_lastRedrawnString = "Invalid";
|
||||
|
||||
_lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})";
|
||||
}
|
||||
}
|
||||
114
Penumbra/Api/IpcTester/ResolveIpcTester.cs
Normal file
114
Penumbra/Api/IpcTester/ResolveIpcTester.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using Dalamud.Plugin;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class ResolveIpcTester(IDalamudPluginInterface pi) : IUiService
|
||||
{
|
||||
private string _currentResolvePath = string.Empty;
|
||||
private string _currentReversePath = string.Empty;
|
||||
private int _currentReverseIdx;
|
||||
private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], []));
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode("Resolving");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength);
|
||||
ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath,
|
||||
Utf8GamePath.MaxGamePathLength);
|
||||
ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0);
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(ResolveDefaultPath.Label, "Default Collection Resolve");
|
||||
if (_currentResolvePath.Length != 0)
|
||||
ImGui.TextUnformatted(new ResolveDefaultPath(pi).Invoke(_currentResolvePath));
|
||||
|
||||
IpcTester.DrawIntro(ResolveInterfacePath.Label, "Interface Collection Resolve");
|
||||
if (_currentResolvePath.Length != 0)
|
||||
ImGui.TextUnformatted(new ResolveInterfacePath(pi).Invoke(_currentResolvePath));
|
||||
|
||||
IpcTester.DrawIntro(ResolvePlayerPath.Label, "Player Collection Resolve");
|
||||
if (_currentResolvePath.Length != 0)
|
||||
ImGui.TextUnformatted(new ResolvePlayerPath(pi).Invoke(_currentResolvePath));
|
||||
|
||||
IpcTester.DrawIntro(ResolveGameObjectPath.Label, "Game Object Collection Resolve");
|
||||
if (_currentResolvePath.Length != 0)
|
||||
ImGui.TextUnformatted(new ResolveGameObjectPath(pi).Invoke(_currentResolvePath, _currentReverseIdx));
|
||||
|
||||
IpcTester.DrawIntro(ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)");
|
||||
if (_currentReversePath.Length > 0)
|
||||
{
|
||||
var list = new ReverseResolvePlayerPath(pi).Invoke(_currentReversePath);
|
||||
if (list.Length > 0)
|
||||
{
|
||||
ImGui.TextUnformatted(list[0]);
|
||||
if (list.Length > 1 && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
|
||||
}
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)");
|
||||
if (_currentReversePath.Length > 0)
|
||||
{
|
||||
var list = new ReverseResolveGameObjectPath(pi).Invoke(_currentReversePath, _currentReverseIdx);
|
||||
if (list.Length > 0)
|
||||
{
|
||||
ImGui.TextUnformatted(list[0]);
|
||||
if (list.Length > 1 && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
|
||||
}
|
||||
}
|
||||
|
||||
var forwardArray = _currentResolvePath.Length > 0
|
||||
? [_currentResolvePath]
|
||||
: Array.Empty<string>();
|
||||
var reverseArray = _currentReversePath.Length > 0
|
||||
? [_currentReversePath]
|
||||
: Array.Empty<string>();
|
||||
|
||||
IpcTester.DrawIntro(ResolvePlayerPaths.Label, "Resolved Paths (Player)");
|
||||
if (forwardArray.Length > 0 || reverseArray.Length > 0)
|
||||
{
|
||||
var ret = new ResolvePlayerPaths(pi).Invoke(forwardArray, reverseArray);
|
||||
ImGui.TextUnformatted(ConvertText(ret));
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)");
|
||||
if (ImGui.Button("Start"))
|
||||
_task = new ResolvePlayerPathsAsync(pi).Invoke(forwardArray, reverseArray);
|
||||
var hovered = ImGui.IsItemHovered();
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(_task.Status.ToString());
|
||||
if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully)
|
||||
ImGui.SetTooltip(ConvertText(_task.Result));
|
||||
return;
|
||||
|
||||
static string ConvertText((string[], string[][]) data)
|
||||
{
|
||||
var text = string.Empty;
|
||||
if (data.Item1.Length > 0)
|
||||
{
|
||||
if (data.Item2.Length > 0)
|
||||
text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}.";
|
||||
else
|
||||
text = $"Forward: {data.Item1[0]}.";
|
||||
}
|
||||
else if (data.Item2.Length > 0)
|
||||
{
|
||||
text = $"Reverse: {string.Join("; ", data.Item2[0])}.";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
350
Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs
Normal file
350
Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class ResourceTreeIpcTester(IDalamudPluginInterface pi, ObjectManager objects) : IUiService
|
||||
{
|
||||
private readonly Stopwatch _stopwatch = new();
|
||||
|
||||
private string _gameObjectIndices = "0";
|
||||
private ResourceType _type = ResourceType.Mtrl;
|
||||
private bool _withUiData;
|
||||
|
||||
private (string, Dictionary<string, HashSet<string>>?)[]? _lastGameObjectResourcePaths;
|
||||
private (string, Dictionary<string, HashSet<string>>?)[]? _lastPlayerResourcePaths;
|
||||
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastGameObjectResourcesOfType;
|
||||
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastPlayerResourcesOfType;
|
||||
private (string, ResourceTreeDto?)[]? _lastGameObjectResourceTrees;
|
||||
private (string, ResourceTreeDto)[]? _lastPlayerResourceTrees;
|
||||
private TimeSpan _lastCallDuration;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Resource Tree");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511);
|
||||
ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues<ResourceType>());
|
||||
ImGui.Checkbox("Also get names and icons", ref _withUiData);
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(GetGameObjectResourcePaths.Label, "Get GameObject resource paths");
|
||||
if (ImGui.Button("Get##GameObjectResourcePaths"))
|
||||
{
|
||||
var gameObjects = GetSelectedGameObjects();
|
||||
var subscriber = new GetGameObjectResourcePaths(pi);
|
||||
_stopwatch.Restart();
|
||||
var resourcePaths = subscriber.Invoke(gameObjects);
|
||||
|
||||
_lastCallDuration = _stopwatch.Elapsed;
|
||||
_lastGameObjectResourcePaths = gameObjects
|
||||
.Select(i => GameObjectToString(i))
|
||||
.Zip(resourcePaths)
|
||||
.ToArray();
|
||||
|
||||
ImGui.OpenPopup(nameof(GetGameObjectResourcePaths));
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetPlayerResourcePaths.Label, "Get local player resource paths");
|
||||
if (ImGui.Button("Get##PlayerResourcePaths"))
|
||||
{
|
||||
var subscriber = new GetPlayerResourcePaths(pi);
|
||||
_stopwatch.Restart();
|
||||
var resourcePaths = subscriber.Invoke();
|
||||
|
||||
_lastCallDuration = _stopwatch.Elapsed;
|
||||
_lastPlayerResourcePaths = resourcePaths
|
||||
.Select(pair => (GameObjectToString(pair.Key), pair.Value))
|
||||
.ToArray()!;
|
||||
|
||||
ImGui.OpenPopup(nameof(GetPlayerResourcePaths));
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetGameObjectResourcesOfType.Label, "Get GameObject resources of type");
|
||||
if (ImGui.Button("Get##GameObjectResourcesOfType"))
|
||||
{
|
||||
var gameObjects = GetSelectedGameObjects();
|
||||
var subscriber = new GetGameObjectResourcesOfType(pi);
|
||||
_stopwatch.Restart();
|
||||
var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects);
|
||||
|
||||
_lastCallDuration = _stopwatch.Elapsed;
|
||||
_lastGameObjectResourcesOfType = gameObjects
|
||||
.Select(i => GameObjectToString(i))
|
||||
.Zip(resourcesOfType)
|
||||
.ToArray();
|
||||
|
||||
ImGui.OpenPopup(nameof(GetGameObjectResourcesOfType));
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetPlayerResourcesOfType.Label, "Get local player resources of type");
|
||||
if (ImGui.Button("Get##PlayerResourcesOfType"))
|
||||
{
|
||||
var subscriber = new GetPlayerResourcesOfType(pi);
|
||||
_stopwatch.Restart();
|
||||
var resourcesOfType = subscriber.Invoke(_type, _withUiData);
|
||||
|
||||
_lastCallDuration = _stopwatch.Elapsed;
|
||||
_lastPlayerResourcesOfType = resourcesOfType
|
||||
.Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)pair.Value))
|
||||
.ToArray();
|
||||
|
||||
ImGui.OpenPopup(nameof(GetPlayerResourcesOfType));
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetGameObjectResourceTrees.Label, "Get GameObject resource trees");
|
||||
if (ImGui.Button("Get##GameObjectResourceTrees"))
|
||||
{
|
||||
var gameObjects = GetSelectedGameObjects();
|
||||
var subscriber = new GetGameObjectResourceTrees(pi);
|
||||
_stopwatch.Restart();
|
||||
var trees = subscriber.Invoke(_withUiData, gameObjects);
|
||||
|
||||
_lastCallDuration = _stopwatch.Elapsed;
|
||||
_lastGameObjectResourceTrees = gameObjects
|
||||
.Select(i => GameObjectToString(i))
|
||||
.Zip(trees)
|
||||
.ToArray();
|
||||
|
||||
ImGui.OpenPopup(nameof(GetGameObjectResourceTrees));
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(GetPlayerResourceTrees.Label, "Get local player resource trees");
|
||||
if (ImGui.Button("Get##PlayerResourceTrees"))
|
||||
{
|
||||
var subscriber = new GetPlayerResourceTrees(pi);
|
||||
_stopwatch.Restart();
|
||||
var trees = subscriber.Invoke(_withUiData);
|
||||
|
||||
_lastCallDuration = _stopwatch.Elapsed;
|
||||
_lastPlayerResourceTrees = trees
|
||||
.Select(pair => (GameObjectToString(pair.Key), pair.Value))
|
||||
.ToArray();
|
||||
|
||||
ImGui.OpenPopup(nameof(GetPlayerResourceTrees));
|
||||
}
|
||||
|
||||
DrawPopup(nameof(GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths,
|
||||
_lastCallDuration);
|
||||
DrawPopup(nameof(GetPlayerResourcePaths), ref _lastPlayerResourcePaths!, DrawResourcePaths, _lastCallDuration);
|
||||
|
||||
DrawPopup(nameof(GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType,
|
||||
_lastCallDuration);
|
||||
DrawPopup(nameof(GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType,
|
||||
_lastCallDuration);
|
||||
|
||||
DrawPopup(nameof(GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees,
|
||||
_lastCallDuration);
|
||||
DrawPopup(nameof(GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration);
|
||||
}
|
||||
|
||||
private static void DrawPopup<T>(string popupId, ref T? result, Action<T> drawResult, TimeSpan duration) where T : class
|
||||
{
|
||||
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500));
|
||||
using var popup = ImRaii.Popup(popupId);
|
||||
if (!popup)
|
||||
{
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
drawResult(result);
|
||||
|
||||
ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms");
|
||||
|
||||
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
|
||||
{
|
||||
result = null;
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawWithHeaders<T>((string, T?)[] result, Action<T> drawItem) where T : class
|
||||
{
|
||||
var firstSeen = new Dictionary<T, string>();
|
||||
foreach (var (label, item) in result)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstSeen.TryGetValue(item, out var firstLabel))
|
||||
{
|
||||
ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
firstSeen.Add(item, label);
|
||||
|
||||
using var header = ImRaii.TreeNode(label);
|
||||
if (!header)
|
||||
continue;
|
||||
|
||||
drawItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawResourcePaths((string, Dictionary<string, HashSet<string>>?)[] result)
|
||||
{
|
||||
DrawWithHeaders(result, paths =>
|
||||
{
|
||||
using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f);
|
||||
ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
foreach (var (actualPath, gamePaths) in paths)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(actualPath);
|
||||
ImGui.TableNextColumn();
|
||||
foreach (var gamePath in gamePaths)
|
||||
ImGui.TextUnformatted(gamePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void DrawResourcesOfType((string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[] result)
|
||||
{
|
||||
DrawWithHeaders(result, resources =>
|
||||
{
|
||||
using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f);
|
||||
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f);
|
||||
if (_withUiData)
|
||||
ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
foreach (var (resourceHandle, (actualPath, name, icon)) in resources)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
TextUnformattedMono($"0x{resourceHandle:X}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(actualPath);
|
||||
if (_withUiData)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
TextUnformattedMono(icon.ToString());
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void DrawResourceTrees((string, ResourceTreeDto?)[] result)
|
||||
{
|
||||
DrawWithHeaders(result, tree =>
|
||||
{
|
||||
ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}");
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
if (_withUiData)
|
||||
{
|
||||
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f);
|
||||
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f);
|
||||
ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f);
|
||||
}
|
||||
|
||||
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
|
||||
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
|
||||
ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f);
|
||||
ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
void DrawNode(ResourceNodeDto node)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
var hasChildren = node.Children.Any();
|
||||
using var treeNode = ImRaii.TreeNode(
|
||||
$"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}",
|
||||
hasChildren
|
||||
? ImGuiTreeNodeFlags.SpanFullWidth
|
||||
: ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen);
|
||||
if (_withUiData)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
TextUnformattedMono(node.Type.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
TextUnformattedMono(node.Icon.ToString());
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(node.GamePath ?? "Unknown");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(node.ActualPath);
|
||||
ImGui.TableNextColumn();
|
||||
TextUnformattedMono($"0x{node.ObjectAddress:X8}");
|
||||
ImGui.TableNextColumn();
|
||||
TextUnformattedMono($"0x{node.ResourceHandle:X8}");
|
||||
|
||||
if (treeNode)
|
||||
foreach (var child in node.Children)
|
||||
DrawNode(child);
|
||||
}
|
||||
|
||||
foreach (var node in tree.Nodes)
|
||||
DrawNode(node);
|
||||
});
|
||||
}
|
||||
|
||||
private static void TextUnformattedMono(string text)
|
||||
{
|
||||
using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||
ImGui.TextUnformatted(text);
|
||||
}
|
||||
|
||||
private ushort[] GetSelectedGameObjects()
|
||||
=> _gameObjectIndices.Split(',')
|
||||
.SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i))
|
||||
.ToArray();
|
||||
|
||||
private unsafe string GameObjectToString(ObjectIndex gameObjectIndex)
|
||||
{
|
||||
var gameObject = objects[gameObjectIndex];
|
||||
|
||||
return gameObject.Valid
|
||||
? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})"
|
||||
: $"[{gameObjectIndex}] null";
|
||||
}
|
||||
}
|
||||
319
Penumbra/Api/IpcTester/TemporaryIpcTester.cs
Normal file
319
Penumbra/Api/IpcTester/TemporaryIpcTester.cs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class TemporaryIpcTester(
|
||||
IDalamudPluginInterface pi,
|
||||
ModManager modManager,
|
||||
CollectionManager collections,
|
||||
TempModManager tempMods,
|
||||
TempCollectionManager tempCollections,
|
||||
SaveService saveService,
|
||||
Configuration config)
|
||||
: IUiService
|
||||
{
|
||||
public Guid LastCreatedCollectionId = Guid.Empty;
|
||||
|
||||
private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9;
|
||||
|
||||
private Guid? _tempGuid;
|
||||
private string _tempCollectionName = string.Empty;
|
||||
private string _tempCollectionGuidName = string.Empty;
|
||||
private string _tempModName = string.Empty;
|
||||
private string _modDirectory = string.Empty;
|
||||
private string _tempGamePath = "test/game/path.mtrl";
|
||||
private string _tempFilePath = "test/success.mtrl";
|
||||
private string _tempManipulation = string.Empty;
|
||||
private string _identity = string.Empty;
|
||||
private PenumbraApiEc _lastTempError;
|
||||
private int _tempActorIndex;
|
||||
private bool _forceOverwrite;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Temporary");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128);
|
||||
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
||||
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
||||
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
||||
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
|
||||
ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256);
|
||||
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
|
||||
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
|
||||
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
|
||||
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro("Last Error", _lastTempError.ToString());
|
||||
ImGuiUtil.DrawTableColumn("Last Created Collection");
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
ImGuiUtil.CopyOnClickSelectable(LastCreatedCollectionId.ToString());
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
|
||||
if (ImGui.Button("Create##Collection"))
|
||||
{
|
||||
_lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
|
||||
if (_tempGuid == null)
|
||||
{
|
||||
_tempGuid = LastCreatedCollectionId;
|
||||
_tempCollectionGuidName = LastCreatedCollectionId.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
var guid = _tempGuid.GetValueOrDefault(Guid.Empty);
|
||||
|
||||
IpcTester.DrawIntro(DeleteTemporaryCollection.Label, "Delete Temporary Collection");
|
||||
if (ImGui.Button("Delete##Collection"))
|
||||
_lastTempError = new DeleteTemporaryCollection(pi).Invoke(guid);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Delete Last##Collection"))
|
||||
_lastTempError = new DeleteTemporaryCollection(pi).Invoke(LastCreatedCollectionId);
|
||||
|
||||
IpcTester.DrawIntro(AssignTemporaryCollection.Label, "Assign Temporary Collection");
|
||||
if (ImGui.Button("Assign##NamedCollection"))
|
||||
_lastTempError = new AssignTemporaryCollection(pi).Invoke(guid, _tempActorIndex, _forceOverwrite);
|
||||
|
||||
IpcTester.DrawIntro(AddTemporaryMod.Label, "Add Temporary Mod to specific Collection");
|
||||
if (ImGui.Button("Add##Mod"))
|
||||
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid,
|
||||
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
|
||||
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
|
||||
|
||||
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Copy Existing Collection");
|
||||
if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
|
||||
"Copies the effective list from the collection named in Temporary Mod Name...",
|
||||
!collections.Storage.ByName(_tempModName, out var copyCollection))
|
||||
&& copyCollection is { HasCache: true })
|
||||
{
|
||||
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
|
||||
var manips = MetaApi.CompressMetaManipulations(copyCollection);
|
||||
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(AddTemporaryModAll.Label, "Add Temporary Mod to all Collections");
|
||||
if (ImGui.Button("Add##All"))
|
||||
_lastTempError = new AddTemporaryModAll(pi).Invoke(_tempModName,
|
||||
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
|
||||
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
|
||||
|
||||
IpcTester.DrawIntro(RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection");
|
||||
if (ImGui.Button("Remove##Mod"))
|
||||
_lastTempError = new RemoveTemporaryMod(pi).Invoke(_tempModName, guid, int.MaxValue);
|
||||
|
||||
IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections");
|
||||
if (ImGui.Button("Remove##ModAll"))
|
||||
_lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue);
|
||||
|
||||
IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection");
|
||||
if (ImUtf8.Button("Set##SetTemporary"u8))
|
||||
_lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337,
|
||||
new Dictionary<string, IReadOnlyList<string>>(),
|
||||
"IPC Tester", 1337);
|
||||
|
||||
IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection");
|
||||
if (ImUtf8.Button("Set##SetTemporaryPlayer"u8))
|
||||
_lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337,
|
||||
new Dictionary<string, IReadOnlyList<string>>(),
|
||||
"IPC Tester", 1337);
|
||||
|
||||
IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection");
|
||||
if (ImUtf8.Button("Remove##RemoveTemporary"u8))
|
||||
_lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1337);
|
||||
ImGui.SameLine();
|
||||
if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8))
|
||||
_lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1338);
|
||||
|
||||
IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection");
|
||||
if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8))
|
||||
_lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1337);
|
||||
ImGui.SameLine();
|
||||
if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8))
|
||||
_lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1338);
|
||||
|
||||
IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection");
|
||||
if (ImUtf8.Button("Remove##RemoveAllTemporary"u8))
|
||||
_lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1337);
|
||||
ImGui.SameLine();
|
||||
if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporary"u8))
|
||||
_lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1338);
|
||||
|
||||
IpcTester.DrawIntro(RemoveAllTemporaryModSettingsPlayer.Label, "Remove All Temporary Mod Settings from game object Collection");
|
||||
if (ImUtf8.Button("Remove##RemoveAllTemporaryPlayer"u8))
|
||||
_lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1337);
|
||||
ImGui.SameLine();
|
||||
if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8))
|
||||
_lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338);
|
||||
|
||||
IpcTester.DrawIntro(QueryTemporaryModSettings.Label, "Query Temporary Mod Settings from specific Collection");
|
||||
ImUtf8.Button("Query##QueryTemporaryModSettings"u8);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
_lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1337);
|
||||
DrawTooltip(settings, source);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporary"u8);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
_lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1338);
|
||||
DrawTooltip(settings, source);
|
||||
}
|
||||
|
||||
IpcTester.DrawIntro(QueryTemporaryModSettingsPlayer.Label, "Query Temporary Mod Settings from game object Collection");
|
||||
ImUtf8.Button("Query##QueryTemporaryModSettingsPlayer"u8);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
_lastTempError =
|
||||
new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1337);
|
||||
DrawTooltip(settings, source);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporaryPlayer"u8);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
_lastTempError =
|
||||
new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1338);
|
||||
DrawTooltip(settings, source);
|
||||
}
|
||||
|
||||
void DrawTooltip((bool ForceInherit, bool Enabled, int Priority, Dictionary<string, List<string>> Settings)? settings, string source)
|
||||
{
|
||||
using var tt = ImUtf8.Tooltip();
|
||||
ImUtf8.Text($"Query returned {_lastTempError}");
|
||||
if (settings != null)
|
||||
ImUtf8.Text($"Settings created by {(source.Length == 0 ? "Unknown Source" : source)}:");
|
||||
else
|
||||
ImUtf8.Text(source.Length > 0 ? $"Locked by {source}." : "No settings exist.");
|
||||
ImGui.Separator();
|
||||
if (settings == null)
|
||||
{
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImUtf8.Group())
|
||||
{
|
||||
ImUtf8.Text("Force Inherit"u8);
|
||||
ImUtf8.Text("Enabled"u8);
|
||||
ImUtf8.Text("Priority"u8);
|
||||
foreach (var group in settings.Value.Settings.Keys)
|
||||
ImUtf8.Text(group);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
using (ImUtf8.Group())
|
||||
{
|
||||
ImUtf8.Text($"{settings.Value.ForceInherit}");
|
||||
ImUtf8.Text($"{settings.Value.Enabled}");
|
||||
ImUtf8.Text($"{settings.Value.Priority}");
|
||||
foreach (var group in settings.Value.Settings.Values)
|
||||
ImUtf8.Text(string.Join("; ", group));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawCollections()
|
||||
{
|
||||
using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8);
|
||||
if (!collTree)
|
||||
return;
|
||||
|
||||
using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
foreach (var (collection, idx) in tempCollections.Values.WithIndex())
|
||||
{
|
||||
using var id = ImRaii.PushId(idx);
|
||||
ImGui.TableNextColumn();
|
||||
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
|
||||
.FirstOrDefault()
|
||||
?? "Unknown";
|
||||
if (_debug && ImUtf8.Button("Save##Collection"u8))
|
||||
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
|
||||
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiUtil.CopyOnClickSelectable(collection.Identity.Identifier);
|
||||
}
|
||||
|
||||
ImGuiUtil.DrawTableColumn(collection.Identity.Name);
|
||||
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
|
||||
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
|
||||
ImGuiUtil.DrawTableColumn(string.Join(", ",
|
||||
tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawMods()
|
||||
{
|
||||
using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods");
|
||||
if (!modTree)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##modTree", 5, ImGuiTableFlags.SizingFixedFit);
|
||||
|
||||
void PrintList(string collectionName, IReadOnlyList<TemporaryMod> list)
|
||||
{
|
||||
foreach (var mod in list)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Name.Text);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Priority.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(collectionName);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Default.Files.Count.ToString());
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using var tt = ImRaii.Tooltip();
|
||||
foreach (var (path, file) in mod.Default.Files)
|
||||
ImGui.TextUnformatted($"{path} -> {file}");
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.TotalManipulations.ToString());
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using var tt = ImRaii.Tooltip();
|
||||
foreach (var identifier in mod.Default.Manipulations.Identifiers)
|
||||
ImGui.TextUnformatted(identifier.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (table)
|
||||
{
|
||||
PrintList("All", tempMods.ModsForAllCollections);
|
||||
foreach (var (collection, list) in tempMods.Mods)
|
||||
PrintList(collection.Identity.Name, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Penumbra/Api/IpcTester/UiIpcTester.cs
Normal file
133
Penumbra/Api/IpcTester/UiIpcTester.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using Dalamud.Plugin;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
||||
public class UiIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
public readonly EventSubscriber<string, float, float> PreSettingsTabBar;
|
||||
public readonly EventSubscriber<string> PreSettingsPanel;
|
||||
public readonly EventSubscriber<string> PostEnabled;
|
||||
public readonly EventSubscriber<string> PostSettingsPanelDraw;
|
||||
public readonly EventSubscriber<ChangedItemType, uint> ChangedItemTooltip;
|
||||
public readonly EventSubscriber<MouseButton, ChangedItemType, uint> ChangedItemClicked;
|
||||
|
||||
private string _lastDrawnMod = string.Empty;
|
||||
private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue;
|
||||
private bool _subscribedToTooltip;
|
||||
private bool _subscribedToClick;
|
||||
private string _lastClicked = string.Empty;
|
||||
private string _lastHovered = string.Empty;
|
||||
private TabType _selectTab = TabType.None;
|
||||
private string _modName = string.Empty;
|
||||
private PenumbraApiEc _ec = PenumbraApiEc.Success;
|
||||
|
||||
public UiIpcTester(IDalamudPluginInterface pi)
|
||||
{
|
||||
_pi = pi;
|
||||
PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod);
|
||||
PreSettingsPanel = IpcSubscribers.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
|
||||
PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod);
|
||||
PostSettingsPanelDraw = IpcSubscribers.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
|
||||
ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip);
|
||||
ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick);
|
||||
PreSettingsTabBar.Disable();
|
||||
PreSettingsPanel.Disable();
|
||||
PostEnabled.Disable();
|
||||
PostSettingsPanelDraw.Disable();
|
||||
ChangedItemTooltip.Disable();
|
||||
ChangedItemClicked.Disable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PreSettingsTabBar.Dispose();
|
||||
PreSettingsPanel.Dispose();
|
||||
PostEnabled.Dispose();
|
||||
PostSettingsPanelDraw.Dispose();
|
||||
ChangedItemTooltip.Dispose();
|
||||
ChangedItemClicked.Dispose();
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("UI");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString()))
|
||||
{
|
||||
if (combo)
|
||||
foreach (var val in Enum.GetValues<TabType>())
|
||||
{
|
||||
if (ImGui.Selectable(val.ToString(), _selectTab == val))
|
||||
_selectTab = val;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256);
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
IpcTester.DrawIntro(IpcSubscribers.PostSettingsDraw.Label, "Last Drawn Mod");
|
||||
ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None");
|
||||
|
||||
IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip");
|
||||
if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip))
|
||||
{
|
||||
if (_subscribedToTooltip)
|
||||
ChangedItemTooltip.Enable();
|
||||
else
|
||||
ChangedItemTooltip.Disable();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_lastHovered);
|
||||
|
||||
IpcTester.DrawIntro(IpcSubscribers.ChangedItemClicked.Label, "Subscribe Click");
|
||||
if (ImGui.Checkbox("##click", ref _subscribedToClick))
|
||||
{
|
||||
if (_subscribedToClick)
|
||||
ChangedItemClicked.Enable();
|
||||
else
|
||||
ChangedItemClicked.Disable();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_lastClicked);
|
||||
IpcTester.DrawIntro(OpenMainWindow.Label, "Open Mod Window");
|
||||
if (ImGui.Button("Open##window"))
|
||||
_ec = new OpenMainWindow(_pi).Invoke(_selectTab, _modName, _modName);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_ec.ToString());
|
||||
|
||||
IpcTester.DrawIntro(CloseMainWindow.Label, "Close Mod Window");
|
||||
if (ImGui.Button("Close##window"))
|
||||
new CloseMainWindow(_pi).Invoke();
|
||||
}
|
||||
|
||||
private void UpdateLastDrawnMod(string name)
|
||||
=> (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
|
||||
|
||||
private void UpdateLastDrawnMod(string name, float _1, float _2)
|
||||
=> (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
|
||||
|
||||
private void AddedTooltip(ChangedItemType type, uint id)
|
||||
{
|
||||
_lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
|
||||
ImGui.TextUnformatted("IPC Test Successful");
|
||||
}
|
||||
|
||||
private void AddedClick(MouseButton button, ChangedItemType type, uint id)
|
||||
{
|
||||
_lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
|
||||
}
|
||||
}
|
||||
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