mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-03 14:23:43 +01:00
Compare commits
2612 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e6a4cb9a58 | ||
|
|
b9ae348529 | ||
|
|
78bb869e67 | ||
|
|
79e6a3b228 | ||
|
|
2fa0677869 | ||
|
|
50d042c104 | ||
|
|
4c90cc84f1 | ||
|
|
da9969d3dd | ||
|
|
720a1dce7b | ||
|
|
acea5b1359 | ||
|
|
74cb08e551 | ||
|
|
70bae7737e | ||
|
|
f984283231 | ||
|
|
9115cbaac1 | ||
|
|
95de9ea48a | ||
|
|
f8f275a962 | ||
|
|
abce14dfdd | ||
|
|
477d61b6ab | ||
|
|
8fd9569508 | ||
|
|
0ce41f82a6 | ||
|
|
7a3c23d8c9 | ||
|
|
32e817d793 | ||
|
|
6902ef48d1 | ||
|
|
c4607a718e | ||
|
|
e1f1a7f378 | ||
|
|
8133e5927f | ||
|
|
f99bfae4bb | ||
|
|
d97e9f37a8 | ||
|
|
062c69385f | ||
|
|
d9418b6743 | ||
|
|
f5e29b96e1 | ||
|
|
57a38aeb94 | ||
|
|
0af0449814 | ||
|
|
958ff5d803 | ||
|
|
ab2ca472fc | ||
|
|
adfe681631 | ||
|
|
885dcbdf04 | ||
|
|
99eb08958c | ||
|
|
653e21e237 | ||
|
|
53ff9a8ab3 | ||
|
|
1d7829593e | ||
|
|
d445df256b | ||
|
|
c66a09dea3 | ||
|
|
f00fe54bb3 | ||
|
|
71a7520e58 | ||
|
|
9ae843731d | ||
|
|
f13893cf77 | ||
|
|
60229f8b45 | ||
|
|
5aeff6d40f | ||
|
|
32cf729aa8 | ||
|
|
549f8ce4b4 | ||
|
|
ec91755065 | ||
|
|
c975336e65 | ||
|
|
f0bdecd472 | ||
|
|
14d8266d69 | ||
|
|
4381b9ef64 | ||
|
|
95e7febd38 | ||
|
|
809422e8d3 | ||
|
|
311882948a | ||
|
|
f17e9be824 | ||
|
|
8ecf7e2381 | ||
|
|
1b4eea4d1e | ||
|
|
b4231d2a2a | ||
|
|
00c11b49f0 | ||
|
|
c4f82435bf | ||
|
|
6ebf550284 | ||
|
|
47cfaf4da2 | ||
|
|
30335fb5d7 | ||
|
|
c49fce4487 | ||
|
|
9dd12f4a71 | ||
|
|
81b3a12341 | ||
|
|
d6d13594e0 | ||
|
|
8422d36e4e | ||
|
|
c64743ee98 | ||
|
|
c330859abc | ||
|
|
9d43895f38 | ||
|
|
37dcd0ba55 | ||
|
|
c6e6c0098c | ||
|
|
8b7dc8fa5b | ||
|
|
fc767589a2 | ||
|
|
c578bd3a49 | ||
|
|
df1a75b58a | ||
|
|
f910211394 | ||
|
|
54f2e5c58f | ||
|
|
2103ae3053 | ||
|
|
58e46accae | ||
|
|
27650708f0 | ||
|
|
c097b634ab | ||
|
|
018be13216 | ||
|
|
1ee4cb99d0 | ||
|
|
1fb2ddaa5e | ||
|
|
35e68e74f4 | ||
|
|
002778f4c1 | ||
|
|
3c5cff1418 | ||
|
|
bcd62cbe69 | ||
|
|
1c7037416c | ||
|
|
f579933dd7 | ||
|
|
61680f0afb | ||
|
|
abd1fd14f5 | ||
|
|
1504af9f3d | ||
|
|
45d8d58ce2 | ||
|
|
086f90171e | ||
|
|
eff6c2e9af | ||
|
|
0f2266963d | ||
|
|
787c19a170 | ||
|
|
5f8eac0ec1 | ||
|
|
cc9f8cc84e | ||
|
|
f6772af246 | ||
|
|
e994163637 | ||
|
|
80c717c9bc | ||
|
|
b104bc3249 | ||
|
|
1f46b4951e | ||
|
|
46c5d52a92 | ||
|
|
5f1dac98d6 | ||
|
|
7fea8d3854 | ||
|
|
28b7bf91bc | ||
|
|
b29a362395 | ||
|
|
10f06e2715 | ||
|
|
1d3a31db6f | ||
|
|
02f1a4cedd | ||
|
|
d2eae54149 | ||
|
|
bf58c6b098 | ||
|
|
c3a3a2cd35 | ||
|
|
1d935def58 | ||
|
|
19e7d1bf50 | ||
|
|
a4a6a650c5 | ||
|
|
34445eb949 | ||
|
|
d45e98a254 | ||
|
|
d89c1abc3b | ||
|
|
35cff163f8 | ||
|
|
fcb29f23c6 | ||
|
|
a37a8eb5aa | ||
|
|
765da6d518 | ||
|
|
ecedacfddb | ||
|
|
2aed252820 | ||
|
|
7390f97d81 | ||
|
|
c97b8e8e9a | ||
|
|
caf19f24cb | ||
|
|
d2b969d996 | ||
|
|
10c4dbc1f8 | ||
|
|
f4ba14de3c | ||
|
|
513a2780f1 | ||
|
|
40a6ec6010 | ||
|
|
78b931ec44 | ||
|
|
48a443921e | ||
|
|
a798eabf67 | ||
|
|
6dbf487c99 | ||
|
|
82ac639854 | ||
|
|
2746f7ea4f | ||
|
|
0a81e39690 | ||
|
|
e52fca05d9 | ||
|
|
3014f7b246 | ||
|
|
b71552607e | ||
|
|
3b2876a6e4 | ||
|
|
7409d0bc2f | ||
|
|
8cfc605ed3 | ||
|
|
cca616bc15 | ||
|
|
afb758e61a | ||
|
|
2d200bcabb | ||
|
|
cd8523a75f | ||
|
|
6e7465aa99 | ||
|
|
d7215adbc3 | ||
|
|
135c067fa7 | ||
|
|
be84b36319 | ||
|
|
cf79f47e08 | ||
|
|
f0131dd5ba | ||
|
|
c0102368c3 | ||
|
|
4b58213597 | ||
|
|
ea4d087ae9 | ||
|
|
4ef8eeb042 | ||
|
|
6776a7fa7e | ||
|
|
eeaaecb855 | ||
|
|
385ce4c7e9 | ||
|
|
06deddcd8a | ||
|
|
1ad7787e4c | ||
|
|
630469fc0e | ||
|
|
81435b4ff2 | ||
|
|
3d7faad2ae | ||
|
|
50a3a20718 | ||
|
|
4b036c6c26 | ||
|
|
ee87098386 | ||
|
|
0ff46fa860 | ||
|
|
e5c4ddf45d | ||
|
|
2500512a6a | ||
|
|
46c8b811ad | ||
|
|
4189d240de | ||
|
|
c247446ba6 | ||
|
|
ced5e344cf | ||
|
|
4613461154 | ||
|
|
d15ebddf18 | ||
|
|
12837bbdca | ||
|
|
9bceed3d57 | ||
|
|
0c3c7ea363 | ||
|
|
e5b739fc52 | ||
|
|
1874de38d0 | ||
|
|
4a206a633e | ||
|
|
1fd31f30ef | ||
|
|
448a745a51 | ||
|
|
f5591f2a4a | ||
|
|
a86a73bbf5 | ||
|
|
976f7840cd | ||
|
|
856c1d089c | ||
|
|
aa0584078b | ||
|
|
17f9c2ee6f | ||
|
|
67de0ccf45 | ||
|
|
b8210e094b | ||
|
|
f3d4ffc40a | ||
|
|
e85d57b094 | ||
|
|
f0af9f1274 | ||
|
|
0b9a48a485 | ||
|
|
d8e3eafa7d | ||
|
|
ba0ef577c5 | ||
|
|
08b7f184e6 | ||
|
|
3ed85b56b5 | ||
|
|
d2f84aa976 | ||
|
|
fc1255661c | ||
|
|
54460c39f3 | ||
|
|
65bbece9cf | ||
|
|
c416d044a4 | ||
|
|
e2a6274b33 | ||
|
|
81e93e0664 | ||
|
|
f24ec8ebe2 | ||
|
|
5e46f43d7d | ||
|
|
cf54bc7f57 | ||
|
|
7795f9a691 | ||
|
|
c390b57b0f | ||
|
|
c9c4447f3d | ||
|
|
15602f5be5 | ||
|
|
e8ee729ec5 | ||
|
|
7b0935750a | ||
|
|
9af4406c8c | ||
|
|
c1859ccb24 | ||
|
|
2e1a11d16c | ||
|
|
60cf7e3c2e | ||
|
|
545536f66f | ||
|
|
c2a030aa6b | ||
|
|
4d6d73abb6 | ||
|
|
fdc84836c9 | ||
|
|
a13fccb9ac | ||
|
|
e8a0ac98ad | ||
|
|
c78725d7d5 | ||
|
|
dbb9931189 | ||
|
|
8dd681bdda | ||
|
|
65bd1d1b52 | ||
|
|
f3b906007d | ||
|
|
da73feacf4 | ||
|
|
48e442a9fd | ||
|
|
069ae772a5 | ||
|
|
8db54ef4f4 | ||
|
|
33db156544 | ||
|
|
c210a4f10a | ||
|
|
d906e5aedf | ||
|
|
a806dd28c3 | ||
|
|
5bfcb71f52 | ||
|
|
1861c40a4f | ||
|
|
7915d516e2 | ||
|
|
ac70f8db89 | ||
|
|
9a0b0bfa0f | ||
|
|
bc47e08e08 | ||
|
|
2877e9f22f | ||
|
|
9f6729dd0b | ||
|
|
1e5776a481 | ||
|
|
b6ed27e235 | ||
|
|
519543772c | ||
|
|
9c0fc8a8c7 | ||
|
|
0eff4e2e67 | ||
|
|
4a4d93baf3 | ||
|
|
2cece9c422 | ||
|
|
2b0844a21e | ||
|
|
efc21c7882 | ||
|
|
b08bf388cc | ||
|
|
7540694050 | ||
|
|
98b4b29ff5 | ||
|
|
ad55d178d4 | ||
|
|
3ef3e75c6a | ||
|
|
4888bc243f | ||
|
|
d03a3168b0 | ||
|
|
d07355c0f8 | ||
|
|
6949011acf | ||
|
|
c7344efdc2 | ||
|
|
5ed80c753f | ||
|
|
0ba0c6d057 | ||
|
|
e6752ade04 | ||
|
|
581b91b337 | ||
|
|
8d2e84eecf | ||
|
|
e7282384f5 | ||
|
|
6f527a1dbc | ||
|
|
707570615c | ||
|
|
de082439a4 | ||
|
|
46581780e0 | ||
|
|
f5fccb0235 | ||
|
|
5d77cd5514 | ||
|
|
c3454f1d16 | ||
|
|
0e8f839471 | ||
|
|
e15d844d4b | ||
|
|
1658102c34 | ||
|
|
9130932a7f | ||
|
|
53d19ff473 | ||
|
|
1ee7a7cbf2 | ||
|
|
8865ff5e79 | ||
|
|
18384a9386 | ||
|
|
6d3a4f08c5 | ||
|
|
c8293c9a6b | ||
|
|
4a9b08de98 | ||
|
|
6fa79c62c4 | ||
|
|
bf40c2a3cb | ||
|
|
6df82fdf18 | ||
|
|
7e7e74a534 | ||
|
|
689a4c73d9 | ||
|
|
564a4195cb | ||
|
|
733b60faae | ||
|
|
7beee50fdf | ||
|
|
f4f3a4dfc1 | ||
|
|
38ad1eef96 | ||
|
|
3d6d3ed2d6 | ||
|
|
25b65ce628 | ||
|
|
a9065d97d3 | ||
|
|
aa180dcdf6 | ||
|
|
947e40b1eb | ||
|
|
26258b7bcc | ||
|
|
09c92ef0b1 | ||
|
|
a62fa06b03 | ||
|
|
1d5ddb0590 | ||
|
|
b6a7a59f92 | ||
|
|
685772e6ac | ||
|
|
b6817c47ed | ||
|
|
40bb7567dd | ||
|
|
75823d413e | ||
|
|
076be3925c | ||
|
|
e18fcafc51 | ||
|
|
ac2f2cf3b9 | ||
|
|
7f60d47c0e | ||
|
|
203550f58d | ||
|
|
e435ec5893 | ||
|
|
7495b3b10b | ||
|
|
6b509d66cb | ||
|
|
62459c058d | ||
|
|
84fd0262c2 | ||
|
|
c542c79923 | ||
|
|
7f9ca5db76 | ||
|
|
3e5ea0d89c | ||
|
|
aa7d71530d | ||
|
|
19b295bbc3 | ||
|
|
f601812666 | ||
|
|
a1f02975cb | ||
|
|
59fa4c4fe4 | ||
|
|
0823423eda | ||
|
|
8064376d3d | ||
|
|
7b5e5f6815 | ||
|
|
cd1faf5860 | ||
|
|
ece034cafc | ||
|
|
12d483e010 | ||
|
|
2c22e37666 | ||
|
|
3458a2920b | ||
|
|
dbcd847736 | ||
|
|
393859232d | ||
|
|
f66e08431c | ||
|
|
63268512db | ||
|
|
1d254e5856 | ||
|
|
3bcd7a44da | ||
|
|
fa12682da6 | ||
|
|
0e552d24a6 | ||
|
|
5802f6b6d7 | ||
|
|
f79405e656 | ||
|
|
3128c2017d | ||
|
|
87e8f2599c | ||
|
|
88a1e9f2ae | ||
|
|
cd74a41453 | ||
|
|
ba2ffcc790 | ||
|
|
743f83d12e | ||
|
|
906e057943 | ||
|
|
e488506cde | ||
|
|
702476f43c | ||
|
|
2b0d55bc69 | ||
|
|
572f4f5e6b | ||
|
|
0c0eeec158 | ||
|
|
982385ccbb | ||
|
|
5084bf603c | ||
|
|
b901c9e744 | ||
|
|
78bef3dec0 | ||
|
|
4a82e6faf1 | ||
|
|
795d605d3f | ||
|
|
39fbf128c2 | ||
|
|
8ee15e4ea2 | ||
|
|
be404c79b3 | ||
|
|
0c135b574b | ||
|
|
b6304d43db | ||
|
|
0b8a3d2d11 |
669 changed files with 84482 additions and 18680 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,11 +10,15 @@ jobs:
|
|||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
dotnet-version: 5.0.100
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
|
|
@ -25,9 +29,9 @@ jobs:
|
|||
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/*
|
||||
|
|
|
|||
63
.github/workflows/release.yml
vendored
63
.github/workflows/release.yml
vendored
|
|
@ -2,18 +2,22 @@ name: Create Release
|
|||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
tags-ignore:
|
||||
- testing_*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
dotnet-version: 5.0.100
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
|
|
@ -22,22 +26,23 @@ jobs:
|
|||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||
- name: Build
|
||||
run: |
|
||||
$ver = '${{ github.ref }}' -replace 'refs/tags/',''
|
||||
invoke-expression 'dotnet build --no-restore --configuration Release --nologo -p:Version=$ver -p:FileVersion=$ver'
|
||||
- name: write version into json
|
||||
$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 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
|
||||
|
|
@ -61,20 +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 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 commit -m "[CI] Updating repo.json for ${{ github.ref_name }}" || true
|
||||
git push origin master
|
||||
|
|
|
|||
87
.github/workflows/test_release.yml
vendored
Normal file
87
.github/workflows/test_release.yml
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
name: Create Test Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- testing_*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
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/stg/latest.zip -OutFile latest.zip
|
||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||
- name: Build
|
||||
run: |
|
||||
$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_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/* -DestinationPath Penumbra.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Penumbra/bin/Debug/*
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Penumbra ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: ./Penumbra.zip
|
||||
asset_name: Penumbra.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Write out repo.json
|
||||
run: |
|
||||
$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 branch -f master ${{ github.sha }}
|
||||
git checkout master
|
||||
git add repo.json
|
||||
git commit -m "[CI] Updating repo.json for ${{ github.ref_name }}" || true
|
||||
git push origin master
|
||||
16
.gitmodules
vendored
Normal file
16
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[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
|
||||
1
OtterGui
Submodule
1
OtterGui
Submodule
|
|
@ -0,0 +1 @@
|
|||
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,43 +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,125 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
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 EquipSlotEnumExtension
|
||||
{
|
||||
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 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 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,21 +0,0 @@
|
|||
namespace Penumbra.GameData.Enums
|
||||
{
|
||||
public enum ObjectType : byte
|
||||
{
|
||||
Unknown,
|
||||
Vfx,
|
||||
DemiHuman,
|
||||
Accessory,
|
||||
World,
|
||||
Housing,
|
||||
Monster,
|
||||
Icon,
|
||||
LoadingScreen,
|
||||
Map,
|
||||
Interface,
|
||||
Equipment,
|
||||
Character,
|
||||
Weapon,
|
||||
Font,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,453 +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,
|
||||
VieraFemale = 1801,
|
||||
VieraFemaleNpc = 1804,
|
||||
UnknownMaleNpc = 9104,
|
||||
UnknownFemaleNpc = 9204,
|
||||
}
|
||||
|
||||
public static class RaceEnumExtensions
|
||||
{
|
||||
public static int ToRspIndex( this SubRace subRace )
|
||||
{
|
||||
return subRace switch
|
||||
{
|
||||
SubRace.Midlander => 0,
|
||||
SubRace.Highlander => 1,
|
||||
SubRace.Wildwood => 10,
|
||||
SubRace.Duskwight => 11,
|
||||
SubRace.Plainsfolk => 20,
|
||||
SubRace.Dunesfolk => 21,
|
||||
SubRace.SeekerOfTheSun => 30,
|
||||
SubRace.KeeperOfTheMoon => 31,
|
||||
SubRace.Seawolf => 40,
|
||||
SubRace.Hellsguard => 41,
|
||||
SubRace.Raen => 50,
|
||||
SubRace.Xaela => 51,
|
||||
SubRace.Helion => 60,
|
||||
SubRace.Lost => 61,
|
||||
SubRace.Rava => 70,
|
||||
SubRace.Veena => 71,
|
||||
_ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ),
|
||||
};
|
||||
}
|
||||
|
||||
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(),
|
||||
_ => throw new ArgumentOutOfRangeException( nameof( modelRace ), modelRace, null ),
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
_ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ),
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToName( this Gender gender )
|
||||
{
|
||||
return gender switch
|
||||
{
|
||||
Gender.Male => "Male",
|
||||
Gender.Female => "Female",
|
||||
Gender.MaleNpc => "Male (NPC)",
|
||||
Gender.FemaleNpc => "Female (NPC)",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
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.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.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,
|
||||
"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,
|
||||
_ => 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,
|
||||
_ => 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.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.Viera => GenderRace.VieraFemaleNpc,
|
||||
_ => GenderRace.Unknown,
|
||||
},
|
||||
_ => GenderRace.Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
namespace Penumbra.GameData.Enums
|
||||
{
|
||||
public enum RedrawType
|
||||
{
|
||||
WithoutSettings,
|
||||
WithSettings,
|
||||
OnlyWithSettings,
|
||||
Unload,
|
||||
RedrawWithoutSettings,
|
||||
RedrawWithSettings,
|
||||
AfterGPoseWithSettings,
|
||||
AfterGPoseWithoutSettings,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,51 +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,332 +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*)\.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/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[ "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 = byte.Parse( groups[ "variant" ].Value );
|
||||
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 id = uint.Parse( groups[ "id" ].Value );
|
||||
if( !groups[ "lang" ].Success )
|
||||
{
|
||||
return GameObjectInfo.Icon( fileType, id, hq );
|
||||
}
|
||||
|
||||
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, 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,325 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Dalamud;
|
||||
using Dalamud.Data;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.GameData.Util;
|
||||
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 void IdentifyParsed( IDictionary< string, object? > set, GameObjectInfo info )
|
||||
{
|
||||
switch( info.ObjectType )
|
||||
{
|
||||
case ObjectType.Unknown:
|
||||
case ObjectType.LoadingScreen:
|
||||
case ObjectType.Map:
|
||||
case ObjectType.Interface:
|
||||
case ObjectType.Vfx:
|
||||
case ObjectType.World:
|
||||
case ObjectType.Housing:
|
||||
case ObjectType.DemiHuman:
|
||||
case ObjectType.Monster:
|
||||
case ObjectType.Icon:
|
||||
case ObjectType.Font:
|
||||
// Don't do anything for these cases.
|
||||
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 ";
|
||||
if( info.CustomizationType == CustomizationType.Skin )
|
||||
{
|
||||
set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var customizationString =
|
||||
$"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
|
||||
set[ customizationString ] = null;
|
||||
}
|
||||
|
||||
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,48 +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>
|
||||
</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"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
{
|
||||
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
|
||||
public readonly struct CharacterArmor
|
||||
{
|
||||
public readonly SetId Set;
|
||||
public readonly byte Variant;
|
||||
public readonly StainId Stain;
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Set},{Variant},{Stain}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +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 = 0x0F08;
|
||||
public const int OffWeaponOffset = 0x0F70;
|
||||
public const int EquipmentOffset = 0x1040;
|
||||
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,107 +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 )
|
||||
{
|
||||
return 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 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,308 +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,
|
||||
_2 = 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,
|
||||
_14 = 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
|
||||
{
|
||||
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._2 => 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._14 => 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,
|
||||
EqpEntry._58 => EquipSlot.Head,
|
||||
EqpEntry._59 => EquipSlot.Head,
|
||||
EqpEntry._60 => EquipSlot.Head,
|
||||
EqpEntry._61 => EquipSlot.Head,
|
||||
EqpEntry._62 => EquipSlot.Head,
|
||||
EqpEntry._63 => EquipSlot.Head,
|
||||
|
||||
_ => EquipSlot.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToLocalName( this EqpEntry entry )
|
||||
{
|
||||
return entry switch
|
||||
{
|
||||
EqpEntry.BodyEnabled => "Enabled",
|
||||
EqpEntry.BodyHideWaist => "Hide Waist",
|
||||
EqpEntry._2 => "Unknown 2",
|
||||
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._14 => "Unknown 14",
|
||||
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 IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >()
|
||||
{
|
||||
[ EquipSlot.Body ] = EqpAttributesBody,
|
||||
[ EquipSlot.Legs ] = EqpAttributesLegs,
|
||||
[ EquipSlot.Hands ] = EqpAttributesHands,
|
||||
[ EquipSlot.Feet ] = EqpAttributesFeet,
|
||||
[ EquipSlot.Head ] = EqpAttributesHead,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,160 +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, ClientLanguage lang = ClientLanguage.English )
|
||||
=> new()
|
||||
{
|
||||
FileType = type,
|
||||
ObjectType = ObjectType.Map,
|
||||
IconId = iconId,
|
||||
IconHq = hq,
|
||||
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 bool IconHq; // 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,92 +0,0 @@
|
|||
using System.IO;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
{
|
||||
public struct GmpEntry
|
||||
{
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -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,105 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
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 != null && path.Length < MaxGamePathLength )
|
||||
{
|
||||
_path = Lower( Trim( ReplaceSlash( path ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
_path = "";
|
||||
}
|
||||
}
|
||||
|
||||
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.Substring( idx + 1 );
|
||||
}
|
||||
|
||||
public int CompareTo( object? rhs )
|
||||
{
|
||||
return rhs switch
|
||||
{
|
||||
string path => string.Compare( _path, path, StringComparison.InvariantCulture ),
|
||||
GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ),
|
||||
_ => -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,62 +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 = null;
|
||||
|
||||
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.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,31 +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, 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,277 +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 class PlayerWatchBase : IDisposable
|
||||
{
|
||||
public const int GPosePlayerIdx = 201;
|
||||
public const int GPoseTableEnd = GPosePlayerIdx + 48;
|
||||
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, (CharacterEquipment, HashSet< PlayerWatcher >) > Equip = new();
|
||||
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;
|
||||
}
|
||||
|
||||
internal void RegisterWatcher( PlayerWatcher watcher )
|
||||
{
|
||||
RegisteredWatchers.Add( watcher );
|
||||
if( watcher.Active )
|
||||
{
|
||||
EnablePlayerWatch();
|
||||
}
|
||||
}
|
||||
|
||||
internal void UnregisterWatcher( PlayerWatcher watcher )
|
||||
{
|
||||
if( RegisteredWatchers.Remove( watcher ) )
|
||||
{
|
||||
foreach( var items in Equip.Values )
|
||||
{
|
||||
items.Item2.Remove( watcher );
|
||||
}
|
||||
}
|
||||
|
||||
CheckActiveStatus();
|
||||
}
|
||||
|
||||
internal void CheckActiveStatus()
|
||||
{
|
||||
if( RegisteredWatchers.Any( w => w.Active ) )
|
||||
{
|
||||
EnablePlayerWatch();
|
||||
}
|
||||
else
|
||||
{
|
||||
DisablePlayerWatch();
|
||||
}
|
||||
}
|
||||
|
||||
internal CharacterEquipment UpdatePlayerWithoutEvent( Character actor )
|
||||
{
|
||||
var equipment = new CharacterEquipment( actor );
|
||||
if( Equip.ContainsKey( actor.Name.ToString() ) )
|
||||
{
|
||||
Equip[ actor.Name.ToString() ] = ( equipment, Equip[ actor.Name.ToString() ].Item2 );
|
||||
}
|
||||
|
||||
return equipment;
|
||||
}
|
||||
|
||||
internal void AddPlayerToWatch( string playerName, PlayerWatcher watcher )
|
||||
{
|
||||
if( Equip.TryGetValue( playerName, out var items ) )
|
||||
{
|
||||
items.Item2.Add( watcher );
|
||||
}
|
||||
else
|
||||
{
|
||||
Equip[ playerName ] = ( new CharacterEquipment(), new HashSet< PlayerWatcher > { watcher } );
|
||||
}
|
||||
}
|
||||
|
||||
public void RemovePlayerFromWatch( string playerName, PlayerWatcher watcher )
|
||||
{
|
||||
if( Equip.TryGetValue( playerName, out var items ) )
|
||||
{
|
||||
items.Item2.Remove( watcher );
|
||||
if( items.Item2.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.Item1.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.Item2, ( 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 (CharacterEquipment, HashSet< PlayerWatcher >) equip )
|
||||
{
|
||||
equip = default;
|
||||
var name = gameObject.Name.ToString();
|
||||
return name.Length != 0 && Equip.TryGetValue( name, out equip );
|
||||
}
|
||||
|
||||
private static bool InvalidObjectKind( ObjectKind kind )
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
ObjectKind.BattleNpc => false,
|
||||
ObjectKind.EventNpc => false,
|
||||
ObjectKind.Player => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private GameObject? GetNextObject()
|
||||
{
|
||||
if( _frameTicker == GPosePlayerIdx - 1 )
|
||||
_frameTicker = GPoseTableEnd;
|
||||
else if( _frameTicker == _objects.Length - 1 )
|
||||
_frameTicker = 0;
|
||||
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 equip ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var character = CheckGPoseObject( actor );
|
||||
if( _cancel )
|
||||
{
|
||||
_cancel = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if( character == null || character.ModelType() != 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginLog.Verbose( "Comparing Gear for {PlayerName} at {Address}...", character.Name, character.Address );
|
||||
if( !equip.Item1.CompareAndUpdate( character ) )
|
||||
{
|
||||
TriggerEvents( equip.Item2, character );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { get; } = 2;
|
||||
|
||||
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, CharacterEquipment) > WatchedPlayers()
|
||||
{
|
||||
CheckValidity();
|
||||
return _playerWatch!.Equip
|
||||
.Where( kvp => kvp.Value.Item2.Contains( this ) )
|
||||
.Select( kvp => ( kvp.Key, kvp.Value.Item1 ) );
|
||||
}
|
||||
}
|
||||
|
||||
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
|
|
@ -1,41 +1,98 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29709.97
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32210.308
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra", "Penumbra\Penumbra.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}"
|
||||
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}"
|
||||
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
|
||||
{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
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using EmbedIO;
|
||||
using EmbedIO.Routing;
|
||||
using EmbedIO.WebApi;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Api
|
||||
{
|
||||
public class ModsController : WebApiController
|
||||
{
|
||||
private readonly Penumbra _penumbra;
|
||||
|
||||
public ModsController( Penumbra penumbra )
|
||||
=> _penumbra = penumbra;
|
||||
|
||||
[Route( HttpVerbs.Get, "/mods" )]
|
||||
public object? GetMods()
|
||||
{
|
||||
var modManager = Service< ModManager >.Get();
|
||||
return modManager.Collections.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new
|
||||
{
|
||||
x.Settings.Enabled,
|
||||
x.Settings.Priority,
|
||||
x.Data.BasePath.Name,
|
||||
x.Data.Meta,
|
||||
BasePath = x.Data.BasePath.FullName,
|
||||
Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ),
|
||||
} )
|
||||
?? null;
|
||||
}
|
||||
|
||||
[Route( HttpVerbs.Post, "/mods" )]
|
||||
public object CreateMod()
|
||||
=> new { };
|
||||
|
||||
[Route( HttpVerbs.Get, "/files" )]
|
||||
public object GetFiles()
|
||||
{
|
||||
var modManager = Service< ModManager >.Get();
|
||||
return modManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
|
||||
o => ( string )o.Key,
|
||||
o => o.Value.FullName
|
||||
)
|
||||
?? new Dictionary< string, string >();
|
||||
}
|
||||
}
|
||||
}
|
||||
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,47 +0,0 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Lumina.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.Api
|
||||
{
|
||||
public interface IPenumbraApiBase
|
||||
{
|
||||
public int ApiVersion { get; }
|
||||
public bool Valid { get; }
|
||||
}
|
||||
|
||||
public delegate void ChangedItemHover( object? item );
|
||||
public delegate void ChangedItemClick( MouseButton button, object? item );
|
||||
|
||||
public interface IPenumbraApi : IPenumbraApiBase
|
||||
{
|
||||
// Triggered when the user hovers over a listed changed object in a mod tab.
|
||||
// Can be used to append tooltips.
|
||||
public event ChangedItemHover? ChangedItemTooltip;
|
||||
// Triggered when the user clicks a listed changed object in a mod tab.
|
||||
public event ChangedItemClick? ChangedItemClicked;
|
||||
|
||||
// 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 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 );
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
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)}";
|
||||
}
|
||||
}
|
||||
103
Penumbra/Api/ModChangedItemAdapter.cs
Normal file
103
Penumbra/Api/ModChangedItemAdapter.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public sealed class ModChangedItemAdapter(WeakReference<ModStorage> storage)
|
||||
: IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>,
|
||||
IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>
|
||||
{
|
||||
IEnumerator<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>
|
||||
IEnumerable<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>.GetEnumerator()
|
||||
=> Storage.Select(m => (m.Identifier, (IReadOnlyDictionary<string, object?>)new ChangedItemDictionaryAdapter(m.ChangedItems)))
|
||||
.GetEnumerator();
|
||||
|
||||
public IEnumerator<KeyValuePair<string, IReadOnlyDictionary<string, object?>>> GetEnumerator()
|
||||
=> Storage.Select(m => new KeyValuePair<string, IReadOnlyDictionary<string, object?>>(m.Identifier,
|
||||
new ChangedItemDictionaryAdapter(m.ChangedItems)))
|
||||
.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> Storage.Count;
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
=> Storage.TryGetMod(key, string.Empty, out _);
|
||||
|
||||
public bool TryGetValue(string key, [NotNullWhen(true)] out IReadOnlyDictionary<string, object?>? value)
|
||||
{
|
||||
if (Storage.TryGetMod(key, string.Empty, out var mod))
|
||||
{
|
||||
value = new ChangedItemDictionaryAdapter(mod.ChangedItems);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, object?> this[string key]
|
||||
=> TryGetValue(key, out var v) ? v : throw new KeyNotFoundException();
|
||||
|
||||
(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)
|
||||
IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
var m = Storage[index];
|
||||
return (m.Identifier, new ChangedItemDictionaryAdapter(m.ChangedItems));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> Keys
|
||||
=> Storage.Select(m => m.Identifier);
|
||||
|
||||
public IEnumerable<IReadOnlyDictionary<string, object?>> Values
|
||||
=> Storage.Select(m => new ChangedItemDictionaryAdapter(m.ChangedItems));
|
||||
|
||||
private ModStorage Storage
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
get => storage.TryGetTarget(out var t)
|
||||
? t
|
||||
: throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed.");
|
||||
}
|
||||
|
||||
private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData> data) : IReadOnlyDictionary<string, object?>
|
||||
{
|
||||
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
|
||||
=> data.Select(d => new KeyValuePair<string, object?>(d.Key, d.Value?.ToInternalObject())).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> data.Count;
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
=> data.ContainsKey(key);
|
||||
|
||||
public bool TryGetValue(string key, out object? value)
|
||||
{
|
||||
if (data.TryGetValue(key, out var v))
|
||||
{
|
||||
value = v?.ToInternalObject();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public object? this[string key]
|
||||
=> data[key]?.ToInternalObject();
|
||||
|
||||
public IEnumerable<string> Keys
|
||||
=> data.Keys;
|
||||
|
||||
public IEnumerable<object?> Values
|
||||
=> data.Values.Select(v => v?.ToInternalObject());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Logging;
|
||||
using Lumina.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Api
|
||||
{
|
||||
public class PenumbraApi : IDisposable, IPenumbraApi
|
||||
{
|
||||
public int ApiVersion { get; } = 3;
|
||||
private Penumbra? _penumbra;
|
||||
private Lumina.GameData? _lumina;
|
||||
|
||||
public bool Valid
|
||||
=> _penumbra != null;
|
||||
|
||||
public PenumbraApi( Penumbra penumbra )
|
||||
{
|
||||
_penumbra = penumbra;
|
||||
_lumina = ( Lumina.GameData? )Dalamud.GameData.GetType()
|
||||
.GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic )
|
||||
?.GetValue( Dalamud.GameData );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_penumbra = null;
|
||||
_lumina = null;
|
||||
}
|
||||
|
||||
public event ChangedItemClick? ChangedItemClicked;
|
||||
public event ChangedItemHover? ChangedItemTooltip;
|
||||
|
||||
internal bool HasTooltip
|
||||
=> ChangedItemTooltip != null;
|
||||
|
||||
internal void InvokeTooltip( object? it )
|
||||
=> ChangedItemTooltip?.Invoke( it );
|
||||
|
||||
internal void InvokeClick( MouseButton button, object? it )
|
||||
=> ChangedItemClicked?.Invoke( button, it );
|
||||
|
||||
|
||||
private void CheckInitialized()
|
||||
{
|
||||
if( !Valid )
|
||||
{
|
||||
throw new Exception( "PluginShare is not initialized." );
|
||||
}
|
||||
}
|
||||
|
||||
public void RedrawObject( string name, RedrawType setting )
|
||||
{
|
||||
CheckInitialized();
|
||||
|
||||
_penumbra!.ObjectReloader.RedrawObject( name, setting );
|
||||
}
|
||||
|
||||
public void RedrawObject( GameObject? gameObject, RedrawType setting )
|
||||
{
|
||||
CheckInitialized();
|
||||
|
||||
_penumbra!.ObjectReloader.RedrawObject( gameObject, setting );
|
||||
}
|
||||
|
||||
public void RedrawAll( RedrawType setting )
|
||||
{
|
||||
CheckInitialized();
|
||||
|
||||
_penumbra!.ObjectReloader.RedrawAll( setting );
|
||||
}
|
||||
|
||||
private static string ResolvePath( string path, ModManager manager, ModCollection collection )
|
||||
{
|
||||
if( !Penumbra.Config.IsEnabled )
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var gamePath = new GamePath( path );
|
||||
var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||
ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||
ret ??= path;
|
||||
return ret;
|
||||
}
|
||||
|
||||
public string ResolvePath( string path )
|
||||
{
|
||||
CheckInitialized();
|
||||
var modManager = Service< ModManager >.Get();
|
||||
return ResolvePath( path, modManager, modManager.Collections.DefaultCollection );
|
||||
}
|
||||
|
||||
public string ResolvePath( string path, string characterName )
|
||||
{
|
||||
CheckInitialized();
|
||||
var modManager = Service< ModManager >.Get();
|
||||
return ResolvePath( path, modManager,
|
||||
modManager.Collections.CharacterCollection.TryGetValue( characterName, out var collection )
|
||||
? collection
|
||||
: ModCollection.Empty );
|
||||
}
|
||||
|
||||
private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource
|
||||
{
|
||||
CheckInitialized();
|
||||
try
|
||||
{
|
||||
if( Path.IsPathRooted( resolvedPath ) )
|
||||
{
|
||||
return _lumina?.GetFileFromDisk< T >( resolvedPath );
|
||||
}
|
||||
|
||||
return Dalamud.GameData.GetFile< T >( resolvedPath );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public T? GetFile< T >( string gamePath ) where T : FileResource
|
||||
=> GetFileIntern< T >( ResolvePath( gamePath ) );
|
||||
|
||||
public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource
|
||||
=> GetFileIntern< T >( ResolvePath( gamePath, characterName ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
using System;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.Api
|
||||
{
|
||||
public class PenumbraIpc : IDisposable
|
||||
{
|
||||
public const string LabelProviderApiVersion = "Penumbra.ApiVersion";
|
||||
public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName";
|
||||
public const string LabelProviderRedrawObject = "Penumbra.RedrawObject";
|
||||
public const string LabelProviderRedrawAll = "Penumbra.RedrawAll";
|
||||
public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath";
|
||||
public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath";
|
||||
|
||||
public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip";
|
||||
public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick";
|
||||
|
||||
internal ICallGateProvider< int >? ProviderApiVersion;
|
||||
internal ICallGateProvider< string, int, object >? ProviderRedrawName;
|
||||
internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject;
|
||||
internal ICallGateProvider< int, object >? ProviderRedrawAll;
|
||||
internal ICallGateProvider< string, string >? ProviderResolveDefault;
|
||||
internal ICallGateProvider< string, string, string >? ProviderResolveCharacter;
|
||||
internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip;
|
||||
internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick;
|
||||
|
||||
internal readonly IPenumbraApi Api;
|
||||
|
||||
private static RedrawType CheckRedrawType( int value )
|
||||
{
|
||||
var type = ( RedrawType )value;
|
||||
if( Enum.IsDefined( type ) )
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
throw new Exception( "The integer provided for a Redraw Function was not a valid RedrawType." );
|
||||
}
|
||||
|
||||
private void OnClick( MouseButton click, object? item )
|
||||
{
|
||||
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item );
|
||||
ProviderChangedItemClick?.SendMessage( click, type, id );
|
||||
}
|
||||
|
||||
private void OnTooltip( object? item )
|
||||
{
|
||||
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item );
|
||||
ProviderChangedItemTooltip?.SendMessage( type, id );
|
||||
}
|
||||
|
||||
|
||||
public PenumbraIpc( DalamudPluginInterface pi, IPenumbraApi api )
|
||||
{
|
||||
Api = api;
|
||||
|
||||
try
|
||||
{
|
||||
ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion );
|
||||
ProviderApiVersion.RegisterFunc( () => api.ApiVersion );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName );
|
||||
ProviderRedrawName.RegisterAction( ( s, i ) => api.RedrawObject( s, CheckRedrawType( i ) ) );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject );
|
||||
ProviderRedrawObject.RegisterAction( ( o, i ) => api.RedrawObject( o, CheckRedrawType( i ) ) );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll );
|
||||
ProviderRedrawAll.RegisterAction( i => api.RedrawAll( CheckRedrawType( i ) ) );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault );
|
||||
ProviderResolveDefault.RegisterFunc( api.ResolvePath );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter );
|
||||
ProviderResolveCharacter.RegisterFunc( api.ResolvePath );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip );
|
||||
api.ChangedItemTooltip += OnTooltip;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemTooltip}:\n{e}" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick );
|
||||
api.ChangedItemClicked += OnClick;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ProviderApiVersion?.UnregisterFunc();
|
||||
ProviderRedrawName?.UnregisterAction();
|
||||
ProviderRedrawObject?.UnregisterAction();
|
||||
ProviderRedrawAll?.UnregisterAction();
|
||||
ProviderResolveDefault?.UnregisterFunc();
|
||||
ProviderResolveCharacter?.UnregisterFunc();
|
||||
Api.ChangedItemClicked -= OnClick;
|
||||
Api.ChangedItemTooltip -= OnTooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
163
Penumbra/Api/TempModManager.cs
Normal file
163
Penumbra/Api/TempModManager.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
public enum RedirectResult
|
||||
{
|
||||
Success = 0,
|
||||
IdenticalFileRegistered = 1,
|
||||
NotRegistered = 2,
|
||||
FilteredGamePath = 3,
|
||||
}
|
||||
|
||||
public class TempModManager : IDisposable, IService
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
private readonly Dictionary<ModCollection, List<TemporaryMod>> _mods = [];
|
||||
private readonly List<TemporaryMod> _modsForAllCollections = [];
|
||||
|
||||
public TempModManager(CommunicatorService communicator)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.TempModManager);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<ModCollection, List<TemporaryMod>> Mods
|
||||
=> _mods;
|
||||
|
||||
public IReadOnlyList<TemporaryMod> ModsForAllCollections
|
||||
=> _modsForAllCollections;
|
||||
|
||||
public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
|
||||
MetaDictionary manips, ModPriority priority)
|
||||
{
|
||||
var mod = GetOrCreateMod(tag, collection, priority, out var created);
|
||||
Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}.");
|
||||
mod.SetAll(dict, manips);
|
||||
ApplyModChange(mod, collection, created, false);
|
||||
return RedirectResult.Success;
|
||||
}
|
||||
|
||||
public RedirectResult Unregister(string tag, ModCollection? collection, ModPriority? priority)
|
||||
{
|
||||
Penumbra.Log.Verbose($"Removing temporary mod with tag {tag}...");
|
||||
var list = collection == null ? _modsForAllCollections : _mods.GetValueOrDefault(collection);
|
||||
if (list == null)
|
||||
return RedirectResult.NotRegistered;
|
||||
|
||||
var removed = list.RemoveAll(m =>
|
||||
{
|
||||
if (m.Name != tag || priority != null && m.Priority != priority.Value)
|
||||
return false;
|
||||
|
||||
ApplyModChange(m, collection, false, true);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (removed == 0)
|
||||
return RedirectResult.NotRegistered;
|
||||
|
||||
if (list.Count == 0 && collection != null)
|
||||
_mods.Remove(collection);
|
||||
|
||||
return RedirectResult.Success;
|
||||
}
|
||||
|
||||
// Apply any new changes to the temporary mod.
|
||||
private void ApplyModChange(TemporaryMod mod, ModCollection? collection, bool created, bool removed)
|
||||
{
|
||||
if (collection != null)
|
||||
{
|
||||
if (removed)
|
||||
{
|
||||
Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.Identity.AnonymizedName}.");
|
||||
collection.Remove(mod);
|
||||
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.Identity.AnonymizedName}.");
|
||||
collection.Apply(mod, created);
|
||||
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Verbose($"Triggering global mod change for {(created ? "new " : string.Empty)}temporary Mod {mod.Name}.");
|
||||
_communicator.TemporaryGlobalModChange.Invoke(mod, created, removed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a mod change to a set of collections.
|
||||
/// </summary>
|
||||
public static void OnGlobalModChange(IEnumerable<ModCollection> collections, TemporaryMod mod, bool created, bool removed)
|
||||
{
|
||||
if (removed)
|
||||
foreach (var c in collections)
|
||||
c.Remove(mod);
|
||||
else
|
||||
foreach (var c in collections)
|
||||
c.Apply(mod, created);
|
||||
}
|
||||
|
||||
// Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections).
|
||||
// Returns the found or created mod and whether it was newly created.
|
||||
private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, ModPriority priority, out bool created)
|
||||
{
|
||||
List<TemporaryMod> list;
|
||||
if (collection == null)
|
||||
{
|
||||
list = _modsForAllCollections;
|
||||
}
|
||||
else if (_mods.TryGetValue(collection, out var l))
|
||||
{
|
||||
list = l;
|
||||
}
|
||||
else
|
||||
{
|
||||
list = [];
|
||||
_mods.Add(collection, list);
|
||||
}
|
||||
|
||||
var mod = list.Find(m => m.Priority == priority && m.Name == tag);
|
||||
if (mod == null)
|
||||
{
|
||||
mod = new TemporaryMod
|
||||
{
|
||||
Name = tag,
|
||||
Priority = priority,
|
||||
};
|
||||
list.Add(mod);
|
||||
created = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
created = false;
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection,
|
||||
string _)
|
||||
{
|
||||
if (collectionType is CollectionType.Temporary or CollectionType.Inactive && newCollection == null && oldCollection != null)
|
||||
_mods.Remove(oldCollection);
|
||||
}
|
||||
}
|
||||
57
Penumbra/ChangedItemMode.cs
Normal file
57
Penumbra/ChangedItemMode.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Text;
|
||||
|
||||
namespace Penumbra;
|
||||
|
||||
public enum ChangedItemMode
|
||||
{
|
||||
GroupedCollapsed,
|
||||
GroupedExpanded,
|
||||
Alphabetical,
|
||||
}
|
||||
|
||||
public static class ChangedItemModeExtensions
|
||||
{
|
||||
public static ReadOnlySpan<byte> ToName(this ChangedItemMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
ChangedItemMode.GroupedCollapsed => "Grouped (Collapsed)"u8,
|
||||
ChangedItemMode.GroupedExpanded => "Grouped (Expanded)"u8,
|
||||
ChangedItemMode.Alphabetical => "Alphabetical"u8,
|
||||
_ => "Error"u8,
|
||||
};
|
||||
|
||||
public static ReadOnlySpan<byte> ToTooltip(this ChangedItemMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
ChangedItemMode.GroupedCollapsed =>
|
||||
"Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item."u8,
|
||||
ChangedItemMode.GroupedExpanded =>
|
||||
"Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item."u8,
|
||||
ChangedItemMode.Alphabetical => "Display all changed items in a single list sorted alphabetically."u8,
|
||||
_ => ""u8,
|
||||
};
|
||||
|
||||
public static bool DrawCombo(ReadOnlySpan<byte> label, ChangedItemMode value, float width, Action<ChangedItemMode> setter)
|
||||
{
|
||||
ImGui.SetNextItemWidth(width);
|
||||
using var combo = ImUtf8.Combo(label, value.ToName());
|
||||
if (!combo)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
foreach (var newValue in Enum.GetValues<ChangedItemMode>())
|
||||
{
|
||||
var selected = ImUtf8.Selectable(newValue.ToName(), newValue == value);
|
||||
if (selected)
|
||||
{
|
||||
ret = true;
|
||||
setter(newValue);
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip(newValue.ToTooltip());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
121
Penumbra/Collections/Cache/AtchCache.cs
Normal file
121
Penumbra/Collections/Cache/AtchCache.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.AtchStructs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class AtchCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtchIdentifier, AtchEntry>(manager, collection)
|
||||
{
|
||||
private readonly Dictionary<GenderRace, (AtchFile, HashSet<AtchIdentifier>)> _atchFiles = [];
|
||||
|
||||
public bool HasFile(GenderRace gr)
|
||||
=> _atchFiles.ContainsKey(gr);
|
||||
|
||||
public bool GetFile(GenderRace gr, [NotNullWhen(true)] out AtchFile? file)
|
||||
{
|
||||
if (!_atchFiles.TryGetValue(gr, out var p))
|
||||
{
|
||||
file = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
file = p.Item1;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (var (_, (_, set)) in _atchFiles)
|
||||
set.Clear();
|
||||
|
||||
_atchFiles.Clear();
|
||||
Clear();
|
||||
}
|
||||
|
||||
protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry)
|
||||
{
|
||||
Collection.Counters.IncrementAtch();
|
||||
ApplyFile(identifier, entry);
|
||||
}
|
||||
|
||||
private void ApplyFile(AtchIdentifier identifier, AtchEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair))
|
||||
{
|
||||
if (!Manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile))
|
||||
throw new Exception($"Invalid Atch File for {identifier.GenderRace.ToName()} requested.");
|
||||
|
||||
pair = (baseFile.Clone(), []);
|
||||
}
|
||||
|
||||
|
||||
if (!Apply(pair.Item1, identifier, entry))
|
||||
return;
|
||||
|
||||
pair.Item2.Add(identifier);
|
||||
_atchFiles[identifier.GenderRace] = pair;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not apply ATCH Manipulation {identifier}:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RevertModInternal(AtchIdentifier identifier)
|
||||
{
|
||||
Collection.Counters.IncrementAtch();
|
||||
if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair))
|
||||
return;
|
||||
|
||||
if (!pair.Item2.Remove(identifier))
|
||||
return;
|
||||
|
||||
if (pair.Item2.Count == 0)
|
||||
{
|
||||
_atchFiles.Remove(identifier.GenderRace);
|
||||
return;
|
||||
}
|
||||
|
||||
var def = GetDefault(Manager, identifier);
|
||||
if (def == null)
|
||||
throw new Exception($"Reverting an .atch mod had no default value for the identifier to revert to.");
|
||||
|
||||
Apply(pair.Item1, identifier, def.Value);
|
||||
}
|
||||
|
||||
public static AtchEntry? GetDefault(MetaFileManager manager, AtchIdentifier identifier)
|
||||
{
|
||||
if (!manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile))
|
||||
return null;
|
||||
|
||||
if (baseFile.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point)
|
||||
return null;
|
||||
|
||||
if (point.Entries.Length <= identifier.EntryIndex)
|
||||
return null;
|
||||
|
||||
return point.Entries[identifier.EntryIndex];
|
||||
}
|
||||
|
||||
public static bool Apply(AtchFile file, AtchIdentifier identifier, in AtchEntry entry)
|
||||
{
|
||||
if (file.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point)
|
||||
return false;
|
||||
|
||||
if (point.Entries.Length <= identifier.EntryIndex)
|
||||
return false;
|
||||
|
||||
point.Entries[identifier.EntryIndex] = entry;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
{
|
||||
Clear();
|
||||
_atchFiles.Clear();
|
||||
}
|
||||
}
|
||||
65
Penumbra/Collections/Cache/AtrCache.cs
Normal file
65
Penumbra/Collections/Cache/AtrCache.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtrIdentifier, AtrEntry>(manager, collection)
|
||||
{
|
||||
public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.CheckEntry(slot, id, genderRace) is false;
|
||||
|
||||
public int EnabledCount { get; private set; }
|
||||
public int DisabledCount { get; private set; }
|
||||
|
||||
|
||||
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> Data
|
||||
=> _atrData;
|
||||
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _atrData = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Clear();
|
||||
_atrData.Clear();
|
||||
DisabledCount = 0;
|
||||
EnabledCount = 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
=> Reset();
|
||||
|
||||
protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry)
|
||||
{
|
||||
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
|
||||
{
|
||||
value = [];
|
||||
_atrData.Add(identifier.Attribute, value);
|
||||
}
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _))
|
||||
{
|
||||
if (entry.Value)
|
||||
++EnabledCount;
|
||||
else
|
||||
++DisabledCount;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RevertModInternal(AtrIdentifier identifier)
|
||||
{
|
||||
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
|
||||
return;
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which))
|
||||
{
|
||||
if (which)
|
||||
--EnabledCount;
|
||||
else
|
||||
--DisabledCount;
|
||||
if (value.IsEmpty)
|
||||
_atrData.Remove(identifier.Attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
547
Penumbra/Collections/Cache/CollectionCache.cs
Normal file
547
Penumbra/Collections/Cache/CollectionCache.cs
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.GameData.Data;
|
||||
using OtterGui.Extensions;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public record struct ModPath(IMod Mod, FullPath Path);
|
||||
public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority, bool Solved);
|
||||
|
||||
/// <summary>
|
||||
/// The Cache contains all required temporary data to use a collection.
|
||||
/// It will only be setup if a collection gets activated in any way.
|
||||
/// </summary>
|
||||
public sealed class CollectionCache : IDisposable
|
||||
{
|
||||
private readonly CollectionCacheManager _manager;
|
||||
private readonly ModCollection _collection;
|
||||
public readonly CollectionModData ModData = new();
|
||||
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> _changedItems = [];
|
||||
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
|
||||
public readonly CustomResourceCache CustomResources;
|
||||
public readonly MetaCache Meta;
|
||||
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
|
||||
|
||||
public int Calculating = -1;
|
||||
|
||||
public string AnonymizedName
|
||||
=> _collection.Identity.AnonymizedName;
|
||||
|
||||
public IEnumerable<SingleArray<ModConflicts>> AllConflicts
|
||||
=> ConflictDict.Values;
|
||||
|
||||
public SingleArray<ModConflicts> Conflicts(IMod mod)
|
||||
=> ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray<ModConflicts>();
|
||||
|
||||
private int _changedItemsSaveCounter = -1;
|
||||
|
||||
// Obtain currently changed items. Computes them if they haven't been computed before.
|
||||
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
}
|
||||
}
|
||||
|
||||
// The cache reacts through events on its collection changing.
|
||||
public CollectionCache(CollectionCacheManager manager, ModCollection collection)
|
||||
{
|
||||
_manager = manager;
|
||||
_collection = collection;
|
||||
Meta = new MetaCache(manager.MetaFileManager, _collection);
|
||||
CustomResources = new CustomResourceCache(manager.ResourceLoader);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Meta.Dispose();
|
||||
CustomResources.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~CollectionCache()
|
||||
=> Dispose();
|
||||
|
||||
// Resolve a given game path according to this collection.
|
||||
public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
|
||||
{
|
||||
if (!ResolvedFiles.TryGetValue(gameResourcePath, out var candidate))
|
||||
return null;
|
||||
|
||||
if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||
|| candidate.Path is { IsRooted: true, Exists: false })
|
||||
return null;
|
||||
|
||||
return candidate.Path;
|
||||
}
|
||||
|
||||
// For a given full path, find all game paths that currently use this file.
|
||||
public IEnumerable<Utf8GamePath> ReverseResolvePath(FullPath localFilePath)
|
||||
{
|
||||
var needle = localFilePath.FullName.ToLower();
|
||||
if (localFilePath.IsRooted)
|
||||
needle = needle.Replace('/', '\\');
|
||||
|
||||
var iterator = ResolvedFiles
|
||||
.Where(f => string.Equals(f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(kvp => kvp.Key);
|
||||
|
||||
// For files that are not rooted, try to add themselves.
|
||||
if (!localFilePath.IsRooted && Utf8GamePath.FromString(localFilePath.FullName, out var utf8))
|
||||
iterator = iterator.Prepend(utf8);
|
||||
|
||||
return iterator;
|
||||
}
|
||||
|
||||
// Reverse resolve multiple paths at once for efficiency.
|
||||
public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> fullPaths)
|
||||
{
|
||||
if (fullPaths.Count == 0)
|
||||
return [];
|
||||
|
||||
var ret = new HashSet<Utf8GamePath>[fullPaths.Count];
|
||||
var dict = new Dictionary<FullPath, int>(fullPaths.Count);
|
||||
foreach (var (path, idx) in fullPaths.WithIndex())
|
||||
{
|
||||
dict[new FullPath(path)] = idx;
|
||||
ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8)
|
||||
? [utf8]
|
||||
: [];
|
||||
}
|
||||
|
||||
foreach (var (game, full) in ResolvedFiles)
|
||||
{
|
||||
if (dict.TryGetValue(full.Path, out var idx))
|
||||
ret[idx].Add(game);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void ReloadMod(IMod mod, bool addMetaChanges)
|
||||
=> _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges));
|
||||
|
||||
public void AddMod(IMod mod, bool addMetaChanges)
|
||||
=> _manager.AddChange(ChangeData.ModAddition(this, mod, addMetaChanges));
|
||||
|
||||
public void RemoveMod(IMod mod, bool addMetaChanges)
|
||||
=> _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges));
|
||||
|
||||
/// <summary> Force a file to be resolved to a specific path regardless of conflicts. </summary>
|
||||
internal void ForceFileSync(Utf8GamePath path, FullPath fullPath)
|
||||
{
|
||||
if (!CheckFullPath(path, fullPath))
|
||||
return;
|
||||
|
||||
if (ResolvedFiles.Remove(path, out var modPath))
|
||||
{
|
||||
ModData.RemovePath(modPath.Mod, path);
|
||||
if (fullPath.FullName.Length > 0)
|
||||
{
|
||||
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
|
||||
CustomResources.Invalidate(path);
|
||||
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path,
|
||||
Mod.ForcedFiles);
|
||||
}
|
||||
else
|
||||
{
|
||||
CustomResources.Invalidate(path);
|
||||
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null);
|
||||
}
|
||||
}
|
||||
else if (fullPath.FullName.Length > 0)
|
||||
{
|
||||
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
|
||||
CustomResources.Invalidate(path);
|
||||
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadModSync(IMod mod, bool addMetaChanges)
|
||||
{
|
||||
RemoveModSync(mod, addMetaChanges);
|
||||
AddModSync(mod, addMetaChanges);
|
||||
}
|
||||
|
||||
internal void RemoveModSync(IMod mod, bool addMetaChanges)
|
||||
{
|
||||
var conflicts = Conflicts(mod);
|
||||
var (paths, manipulations) = ModData.RemoveMod(mod);
|
||||
|
||||
if (addMetaChanges)
|
||||
_collection.Counters.IncrementChange();
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (ResolvedFiles.Remove(path, out var mp))
|
||||
{
|
||||
CustomResources.Invalidate(path);
|
||||
if (mp.Mod != mod)
|
||||
Penumbra.Log.Warning(
|
||||
$"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}.");
|
||||
else
|
||||
_manager.ResolvedFileChanged.Invoke(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, mp.Path, mp.Mod);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var manipulation in manipulations)
|
||||
{
|
||||
if (Meta.RevertMod(manipulation, out var mp) && mp != mod)
|
||||
Penumbra.Log.Warning(
|
||||
$"Invalid mod state, removing {mod.Name} and associated manipulation {manipulation} returned current mod {mp.Name}.");
|
||||
}
|
||||
|
||||
ConflictDict.Remove(mod);
|
||||
foreach (var conflict in conflicts)
|
||||
{
|
||||
if (conflict.HasPriority)
|
||||
{
|
||||
ReloadModSync(conflict.Mod2, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newConflicts = Conflicts(conflict.Mod2).Remove(c => c.Mod2 == mod);
|
||||
if (newConflicts.Count > 0)
|
||||
ConflictDict[conflict.Mod2] = newConflicts;
|
||||
else
|
||||
ConflictDict.Remove(conflict.Mod2);
|
||||
}
|
||||
}
|
||||
|
||||
if (addMetaChanges)
|
||||
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Add all files and possibly manipulations of a given mod according to its settings in this collection. </summary>
|
||||
internal void AddModSync(IMod mod, bool addMetaChanges)
|
||||
{
|
||||
var files = GetFiles(mod);
|
||||
foreach (var (path, file) in files.FileRedirections)
|
||||
AddFile(path, file, mod);
|
||||
|
||||
if (files.Manipulations.Count > 0)
|
||||
{
|
||||
foreach (var (identifier, entry) in files.Manipulations.Eqp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Eqdp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Est)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Gmp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Rsp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Imc)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atch)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Shp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atr)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var identifier in files.Manipulations.GlobalEqp)
|
||||
AddManipulation(mod, identifier, null!);
|
||||
}
|
||||
|
||||
if (addMetaChanges)
|
||||
{
|
||||
_collection.Counters.IncrementChange();
|
||||
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
|
||||
}
|
||||
}
|
||||
|
||||
private AppliedModData GetFiles(IMod mod)
|
||||
{
|
||||
if (mod.Index < 0)
|
||||
return mod.GetData();
|
||||
|
||||
var settings = _collection.GetActualSettings(mod.Index).Settings;
|
||||
return settings is not { Enabled: true }
|
||||
? AppliedModData.Empty
|
||||
: mod.GetData(settings);
|
||||
}
|
||||
|
||||
/// <summary> Invoke only if not in a full recalculation. </summary>
|
||||
private void InvokeResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath key, FullPath value,
|
||||
FullPath old, IMod? mod)
|
||||
{
|
||||
if (Calculating == -1)
|
||||
_manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod);
|
||||
}
|
||||
|
||||
private static bool IsRedirectionSupported(Utf8GamePath path, IMod mod)
|
||||
{
|
||||
var ext = path.Extension().AsciiToLower().ToString();
|
||||
switch (ext)
|
||||
{
|
||||
case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc":
|
||||
Penumbra.Messager.NotificationMessage(
|
||||
$"Redirection of {ext} files for {mod.Name} is unsupported. This probably means that the mod is outdated and may not work correctly.\n\nPlease tell the mod creator to use the corresponding meta manipulations instead.",
|
||||
NotificationType.Warning);
|
||||
return false;
|
||||
case ".lvb" or ".lgb" or ".sgb":
|
||||
Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.\n\nThis mod will probably not work correctly.",
|
||||
NotificationType.Warning);
|
||||
return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a specific file redirection, handling potential conflicts.
|
||||
// For different mods, higher mod priority takes precedence before option group priority,
|
||||
// which takes precedence before option priority, which takes precedence before ordering.
|
||||
// Inside the same mod, conflicts are not recorded.
|
||||
private void AddFile(Utf8GamePath path, FullPath file, IMod mod)
|
||||
{
|
||||
if (!CheckFullPath(path, file))
|
||||
return;
|
||||
|
||||
if (!IsRedirectionSupported(path, mod))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
|
||||
{
|
||||
ModData.AddPath(mod, path);
|
||||
CustomResources.Invalidate(path);
|
||||
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod);
|
||||
return;
|
||||
}
|
||||
|
||||
var modPath = ResolvedFiles[path];
|
||||
// Lower prioritized option in the same mod.
|
||||
if (mod == modPath.Mod)
|
||||
return;
|
||||
|
||||
if (AddConflict(path, mod, modPath.Mod))
|
||||
{
|
||||
ModData.RemovePath(modPath.Mod, path);
|
||||
ResolvedFiles[path] = new ModPath(mod, file);
|
||||
ModData.AddPath(mod, path);
|
||||
CustomResources.Invalidate(path);
|
||||
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error(
|
||||
$"[{Environment.CurrentManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove all empty conflict sets for a given mod with the given conflicts.
|
||||
// If transitive is true, also removes the corresponding version of the other mod.
|
||||
private void RemoveEmptyConflicts(IMod mod, SingleArray<ModConflicts> oldConflicts, bool transitive)
|
||||
{
|
||||
var changedConflicts = oldConflicts.Remove(c =>
|
||||
{
|
||||
if (c.Conflicts.Count == 0)
|
||||
{
|
||||
if (transitive)
|
||||
RemoveEmptyConflicts(c.Mod2, Conflicts(c.Mod2), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
if (changedConflicts.Count == 0)
|
||||
ConflictDict.Remove(mod);
|
||||
else
|
||||
ConflictDict[mod] = changedConflicts;
|
||||
}
|
||||
|
||||
// Add a new conflict between the added mod and the existing mod.
|
||||
// Update all other existing conflicts between the existing mod and other mods if necessary.
|
||||
// Returns if the added mod takes priority before the existing mod.
|
||||
private bool AddConflict(object data, IMod addedMod, IMod existingMod)
|
||||
{
|
||||
var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority;
|
||||
var existingPriority =
|
||||
existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority;
|
||||
|
||||
if (existingPriority < addedPriority)
|
||||
{
|
||||
var tmpConflicts = Conflicts(existingMod);
|
||||
foreach (var conflict in tmpConflicts)
|
||||
{
|
||||
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0
|
||||
|| data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0)
|
||||
AddConflict(data, addedMod, conflict.Mod2);
|
||||
}
|
||||
|
||||
RemoveEmptyConflicts(existingMod, tmpConflicts, true);
|
||||
}
|
||||
|
||||
var addedConflicts = Conflicts(addedMod);
|
||||
var existingConflicts = Conflicts(existingMod);
|
||||
if (addedConflicts.FindFirst(c => c.Mod2 == existingMod, out var oldConflicts))
|
||||
{
|
||||
// Only need to change one list since both conflict lists refer to the same list.
|
||||
oldConflicts.Conflicts.Add(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the same conflict list to both conflict directions.
|
||||
var conflictList = new List<object> { data };
|
||||
ConflictDict[addedMod] = addedConflicts.Append(new ModConflicts(existingMod, conflictList, existingPriority < addedPriority,
|
||||
existingPriority != addedPriority));
|
||||
ConflictDict[existingMod] = existingConflicts.Append(new ModConflicts(addedMod, conflictList,
|
||||
existingPriority >= addedPriority,
|
||||
existingPriority != addedPriority));
|
||||
}
|
||||
|
||||
return existingPriority < addedPriority;
|
||||
}
|
||||
|
||||
// Add a specific manipulation, handling potential conflicts.
|
||||
// For different mods, higher mod priority takes precedence before option group priority,
|
||||
// which takes precedence before option priority, which takes precedence before ordering.
|
||||
// Inside the same mod, conflicts are not recorded.
|
||||
private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry)
|
||||
{
|
||||
if (!Meta.TryGetMod(identifier, out var existingMod))
|
||||
{
|
||||
Meta.ApplyMod(mod, identifier, entry);
|
||||
ModData.AddManip(mod, identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lower prioritized option in the same mod.
|
||||
if (mod == existingMod)
|
||||
return;
|
||||
|
||||
if (AddConflict(identifier, mod, existingMod))
|
||||
{
|
||||
ModData.RemoveManip(existingMod, identifier);
|
||||
Meta.ApplyMod(mod, identifier, entry);
|
||||
ModData.AddManip(mod, identifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Identify and record all manipulated objects for this entire collection.
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if (_changedItemsSaveCounter == _collection.Counters.Change)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_changedItemsSaveCounter = _collection.Counters.Change;
|
||||
_changedItems.Clear();
|
||||
// Skip IMCs because they would result in far too many false-positive items,
|
||||
// since they are per set instead of per item-slot/item/variant.
|
||||
var identifier = _manager.MetaFileManager.Identifier;
|
||||
var items = new SortedList<string, IIdentifiedObjectData>(512);
|
||||
|
||||
void AddItems(IMod mod)
|
||||
{
|
||||
foreach (var (name, obj) in items)
|
||||
{
|
||||
if (!_changedItems.TryGetValue(name, out var data))
|
||||
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
|
||||
else if (!data.Item1.Contains(mod))
|
||||
_changedItems[name] = (data.Item1.Append(mod),
|
||||
obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
|
||||
else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y)
|
||||
_changedItems[name] = (data.Item1, x + y);
|
||||
}
|
||||
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
foreach (var (resolved, modPath) in ResolvedFiles.Where(file => !file.Key.Path.EndsWith("imc"u8)))
|
||||
{
|
||||
identifier.Identify(items, resolved.ToString());
|
||||
AddItems(modPath.Mod);
|
||||
}
|
||||
|
||||
foreach (var (manip, mod) in Meta.IdentifierSources)
|
||||
{
|
||||
manip.AddChangedItems(identifier, items);
|
||||
AddItems(mod);
|
||||
}
|
||||
|
||||
if (_manager.Config.HideMachinistOffhandFromChangedItems)
|
||||
_changedItems.RemoveMachinistOffhands();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Unknown Error:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
|
||||
{
|
||||
if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
|
||||
return true;
|
||||
|
||||
Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
public readonly record struct ChangeData
|
||||
{
|
||||
public readonly CollectionCache Cache;
|
||||
public readonly Utf8GamePath Path;
|
||||
public readonly FullPath FullPath;
|
||||
public readonly IMod Mod;
|
||||
public readonly byte Type;
|
||||
public readonly bool AddMetaChanges;
|
||||
|
||||
private ChangeData(CollectionCache cache, Utf8GamePath p, FullPath fp, IMod m, byte t, bool a)
|
||||
{
|
||||
Cache = cache;
|
||||
Path = p;
|
||||
FullPath = fp;
|
||||
Mod = m;
|
||||
Type = t;
|
||||
AddMetaChanges = a;
|
||||
}
|
||||
|
||||
public static ChangeData ModRemoval(CollectionCache cache, IMod mod, bool addMetaChanges)
|
||||
=> new(cache, Utf8GamePath.Empty, FullPath.Empty, mod, 0, addMetaChanges);
|
||||
|
||||
public static ChangeData ModAddition(CollectionCache cache, IMod mod, bool addMetaChanges)
|
||||
=> new(cache, Utf8GamePath.Empty, FullPath.Empty, mod, 1, addMetaChanges);
|
||||
|
||||
public static ChangeData ModReload(CollectionCache cache, IMod mod, bool addMetaChanges)
|
||||
=> new(cache, Utf8GamePath.Empty, FullPath.Empty, mod, 2, addMetaChanges);
|
||||
|
||||
public static ChangeData ForcedFile(CollectionCache cache, Utf8GamePath p, FullPath fp)
|
||||
=> new(cache, p, fp, Mods.Mod.ForcedFiles, 3, false);
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case 0:
|
||||
Cache.RemoveModSync(Mod, AddMetaChanges);
|
||||
break;
|
||||
case 1:
|
||||
Cache.AddModSync(Mod, AddMetaChanges);
|
||||
break;
|
||||
case 2:
|
||||
Cache.ReloadModSync(Mod, AddMetaChanges);
|
||||
break;
|
||||
case 3:
|
||||
Cache.ForceFileSync(Path, FullPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
411
Penumbra/Collections/Cache/CollectionCacheManager.cs
Normal file
411
Penumbra/Collections/Cache/CollectionCacheManager.cs
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.Manager.OptionEditor;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.Mods.SubMods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public class CollectionCacheManager : IDisposable, IService
|
||||
{
|
||||
private readonly FrameworkManager _framework;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly TempModManager _tempMods;
|
||||
private readonly ModStorage _modStorage;
|
||||
private readonly CollectionStorage _storage;
|
||||
private readonly ActiveCollections _active;
|
||||
internal readonly Configuration Config;
|
||||
internal readonly ResolvedFileChanged ResolvedFileChanged;
|
||||
internal readonly MetaFileManager MetaFileManager;
|
||||
internal readonly ResourceLoader ResourceLoader;
|
||||
|
||||
private readonly ConcurrentQueue<CollectionCache.ChangeData> _changeQueue = new();
|
||||
|
||||
private int _count;
|
||||
|
||||
public int Count
|
||||
=> _count;
|
||||
|
||||
public IEnumerable<ModCollection> Active
|
||||
=> _storage.Where(c => c.HasCache);
|
||||
|
||||
public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage,
|
||||
MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader,
|
||||
Configuration config)
|
||||
{
|
||||
_framework = framework;
|
||||
_communicator = communicator;
|
||||
_tempMods = tempMods;
|
||||
_modStorage = modStorage;
|
||||
MetaFileManager = metaFileManager;
|
||||
_active = active;
|
||||
_storage = storage;
|
||||
ResourceLoader = resourceLoader;
|
||||
Config = config;
|
||||
ResolvedFileChanged = _communicator.ResolvedFileChanged;
|
||||
|
||||
if (!_active.Individuals.IsLoaded)
|
||||
_active.Individuals.Loaded += CreateNecessaryCaches;
|
||||
_framework.Framework.Update += OnFramework;
|
||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionCacheManager);
|
||||
_communicator.ModPathChanged.Subscribe(OnModChangeAddition, ModPathChanged.Priority.CollectionCacheManagerAddition);
|
||||
_communicator.ModPathChanged.Subscribe(OnModChangeRemoval, ModPathChanged.Priority.CollectionCacheManagerRemoval);
|
||||
_communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange, TemporaryGlobalModChange.Priority.CollectionCacheManager);
|
||||
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionCacheManager);
|
||||
_communicator.ModSettingChanged.Subscribe(OnModSettingChange, ModSettingChanged.Priority.CollectionCacheManager);
|
||||
_communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange,
|
||||
CollectionInheritanceChanged.Priority.CollectionCacheManager);
|
||||
_communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionCacheManager);
|
||||
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager);
|
||||
|
||||
if (!MetaFileManager.CharacterUtility.Ready)
|
||||
MetaFileManager.CharacterUtility.LoadingFinished.Subscribe(IncrementCounters, CharacterUtilityFinished.Priority.CollectionCacheManager);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModChangeAddition);
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModChangeRemoval);
|
||||
_communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange);
|
||||
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
|
||||
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
|
||||
_communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange);
|
||||
MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
|
||||
|
||||
foreach (var collection in _storage)
|
||||
{
|
||||
collection._cache?.Dispose();
|
||||
collection._cache = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddChange(CollectionCache.ChangeData data)
|
||||
{
|
||||
if (data.Cache.Calculating == -1)
|
||||
{
|
||||
if (_framework.Framework.IsInFrameworkUpdateThread)
|
||||
data.Apply();
|
||||
else
|
||||
_changeQueue.Enqueue(data);
|
||||
}
|
||||
else if (data.Cache.Calculating == Environment.CurrentManagedThreadId)
|
||||
{
|
||||
data.Apply();
|
||||
}
|
||||
else
|
||||
{
|
||||
_changeQueue.Enqueue(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Only creates a new cache, does not update an existing one. </summary>
|
||||
public bool CreateCache(ModCollection collection)
|
||||
{
|
||||
if (collection.Identity.Index == ModCollection.Empty.Identity.Index)
|
||||
return false;
|
||||
|
||||
if (collection._cache != null)
|
||||
return false;
|
||||
|
||||
collection._cache = new CollectionCache(this, collection);
|
||||
if (collection.Identity.Index > 0)
|
||||
Interlocked.Increment(ref _count);
|
||||
Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the effective file list for the given cache.
|
||||
/// Does not create caches.
|
||||
/// </summary>
|
||||
public void CalculateEffectiveFileList(ModCollection collection)
|
||||
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identity.Identifier,
|
||||
() => CalculateEffectiveFileListInternal(collection));
|
||||
|
||||
private void CalculateEffectiveFileListInternal(ModCollection collection)
|
||||
{
|
||||
// Skip the empty collection.
|
||||
if (collection.Identity.Index == 0)
|
||||
return;
|
||||
|
||||
Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName}");
|
||||
if (!collection.HasCache)
|
||||
{
|
||||
Penumbra.Log.Error(
|
||||
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists.");
|
||||
}
|
||||
else if (collection._cache!.Calculating != -1)
|
||||
{
|
||||
Penumbra.Log.Error(
|
||||
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
|
||||
}
|
||||
else
|
||||
{
|
||||
FullRecalculation(collection);
|
||||
|
||||
Penumbra.Log.Debug(
|
||||
$"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.Identity.AnonymizedName} finished.");
|
||||
}
|
||||
}
|
||||
|
||||
private void FullRecalculation(ModCollection collection)
|
||||
{
|
||||
var cache = collection._cache;
|
||||
if (cache is not { Calculating: -1 })
|
||||
return;
|
||||
|
||||
cache.Calculating = Environment.CurrentManagedThreadId;
|
||||
try
|
||||
{
|
||||
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty,
|
||||
FullPath.Empty, null);
|
||||
cache.ResolvedFiles.Clear();
|
||||
cache.Meta.Reset();
|
||||
cache.ConflictDict.Clear();
|
||||
|
||||
// Add all forced redirects.
|
||||
foreach (var tempMod in _tempMods.ModsForAllCollections
|
||||
.Concat(_tempMods.Mods.TryGetValue(collection, out var list)
|
||||
? list
|
||||
: Array.Empty<TemporaryMod>()))
|
||||
cache.AddModSync(tempMod, false);
|
||||
|
||||
foreach (var mod in _modStorage)
|
||||
cache.AddModSync(mod, false);
|
||||
|
||||
collection.Counters.IncrementChange();
|
||||
|
||||
MetaFileManager.ApplyDefaultFiles(collection);
|
||||
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty,
|
||||
FullPath.Empty,
|
||||
null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache.Calculating = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName)
|
||||
{
|
||||
if (type is CollectionType.Temporary)
|
||||
{
|
||||
if (newCollection != null && CreateCache(newCollection))
|
||||
CalculateEffectiveFileList(newCollection);
|
||||
|
||||
if (old != null)
|
||||
ClearCache(old);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveCache(old);
|
||||
if (type is not CollectionType.Inactive && newCollection != null && newCollection.Identity.Index != 0 && CreateCache(newCollection))
|
||||
CalculateEffectiveFileList(newCollection);
|
||||
|
||||
if (type is CollectionType.Default)
|
||||
if (newCollection != null)
|
||||
MetaFileManager.ApplyDefaultFiles(newCollection);
|
||||
else
|
||||
MetaFileManager.CharacterUtility.ResetAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnModChangeRemoval(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Deleted:
|
||||
case ModPathChangeType.StartingReload:
|
||||
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
|
||||
collection._cache!.RemoveMod(mod, true);
|
||||
break;
|
||||
case ModPathChangeType.Moved:
|
||||
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModChangeAddition(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath)
|
||||
{
|
||||
if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded))
|
||||
return;
|
||||
|
||||
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
|
||||
collection._cache!.AddMod(mod, true);
|
||||
}
|
||||
|
||||
/// <summary> Apply a mod change to all collections with a cache. </summary>
|
||||
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
|
||||
=> TempModManager.OnGlobalModChange(_storage.Where(c => c.HasCache), mod, created, removed);
|
||||
|
||||
/// <summary> Remove a cache from a collection if it is active. </summary>
|
||||
private void RemoveCache(ModCollection? collection)
|
||||
{
|
||||
if (collection != null
|
||||
&& collection.Identity.Index > ModCollection.Empty.Identity.Index
|
||||
&& collection.Identity.Index != _active.Default.Identity.Index
|
||||
&& collection.Identity.Index != _active.Interface.Identity.Index
|
||||
&& collection.Identity.Index != _active.Current.Identity.Index
|
||||
&& _active.SpecialAssignments.All(c => c.Value.Identity.Index != collection.Identity.Index)
|
||||
&& _active.Individuals.All(c => c.Collection.Identity.Index != collection.Identity.Index))
|
||||
ClearCache(collection);
|
||||
}
|
||||
|
||||
/// <summary> Prepare Changes by removing mods from caches with collections or add or reload mods. </summary>
|
||||
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
|
||||
int movedToIdx)
|
||||
{
|
||||
if (type is ModOptionChangeType.PrepareChange)
|
||||
{
|
||||
foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
|
||||
collection._cache!.RemoveMod(mod, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
type.HandlingInfo(out _, out var recomputeList, out var justAdd);
|
||||
|
||||
if (!recomputeList)
|
||||
return;
|
||||
|
||||
foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
|
||||
{
|
||||
if (justAdd)
|
||||
collection._cache!.AddMod(mod, true);
|
||||
else
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Increment the counter to ensure new files are loaded after applying meta changes. </summary>
|
||||
private void IncrementCounters()
|
||||
{
|
||||
foreach (var collection in _storage.Where(c => c.HasCache))
|
||||
collection.Counters.IncrementChange();
|
||||
MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
|
||||
}
|
||||
|
||||
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _)
|
||||
{
|
||||
if (!collection.HasCache)
|
||||
return;
|
||||
|
||||
var cache = collection._cache!;
|
||||
switch (type)
|
||||
{
|
||||
case ModSettingChange.Inheritance:
|
||||
cache.ReloadMod(mod!, true);
|
||||
break;
|
||||
case ModSettingChange.EnableState:
|
||||
if (oldValue == Setting.False)
|
||||
cache.AddMod(mod!, true);
|
||||
else if (oldValue == Setting.True)
|
||||
cache.RemoveMod(mod!, true);
|
||||
else if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
|
||||
cache.ReloadMod(mod!, true);
|
||||
else
|
||||
cache.RemoveMod(mod!, true);
|
||||
|
||||
break;
|
||||
case ModSettingChange.Priority:
|
||||
if (cache.Conflicts(mod!).Count > 0)
|
||||
cache.ReloadMod(mod!, true);
|
||||
|
||||
break;
|
||||
case ModSettingChange.Setting:
|
||||
if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
|
||||
cache.ReloadMod(mod, true);
|
||||
|
||||
break;
|
||||
case ModSettingChange.TemporarySetting:
|
||||
cache.ReloadMod(mod!, true);
|
||||
break;
|
||||
case ModSettingChange.MultiInheritance:
|
||||
case ModSettingChange.MultiEnableState:
|
||||
FullRecalculation(collection);
|
||||
break;
|
||||
case ModSettingChange.TemporaryMod:
|
||||
case ModSettingChange.Edited:
|
||||
// handled otherwise
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inheritance changes are too big to check for relevance,
|
||||
/// just recompute everything.
|
||||
/// </summary>
|
||||
private void OnCollectionInheritanceChange(ModCollection collection, bool _)
|
||||
=> FullRecalculation(collection);
|
||||
|
||||
/// <summary> Clear the current cache of a collection. </summary>
|
||||
private void ClearCache(ModCollection collection)
|
||||
{
|
||||
if (!collection.HasCache)
|
||||
return;
|
||||
|
||||
collection._cache!.Dispose();
|
||||
collection._cache = null;
|
||||
if (collection.Identity.Index > 0)
|
||||
Interlocked.Decrement(ref _count);
|
||||
Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache handling. Usually recreate caches on the next framework tick,
|
||||
/// but at launch create all of them at once.
|
||||
/// </summary>
|
||||
public void CreateNecessaryCaches()
|
||||
{
|
||||
ModCollection[] caches;
|
||||
// Lock to make sure no race conditions during CreateCache happen.
|
||||
lock (this)
|
||||
{
|
||||
caches = _active.SpecialAssignments.Select(p => p.Value)
|
||||
.Concat(_active.Individuals.Select(p => p.Collection))
|
||||
.Prepend(_active.Current)
|
||||
.Prepend(_active.Default)
|
||||
.Prepend(_active.Interface)
|
||||
.Where(CreateCache).ToArray();
|
||||
}
|
||||
|
||||
Parallel.ForEach(caches, CalculateEffectiveFileListInternal);
|
||||
}
|
||||
|
||||
private void OnModDiscoveryStarted()
|
||||
{
|
||||
foreach (var collection in Active)
|
||||
{
|
||||
collection._cache!.ResolvedFiles.Clear();
|
||||
collection._cache!.Meta.Reset();
|
||||
collection._cache!.ConflictDict.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModDiscoveryFinished()
|
||||
=> Parallel.ForEach(Active, CalculateEffectiveFileListInternal);
|
||||
|
||||
/// <summary>
|
||||
/// Update forced files only on framework.
|
||||
/// </summary>
|
||||
private void OnFramework(IFramework _)
|
||||
{
|
||||
while (_changeQueue.TryDequeue(out var changeData))
|
||||
changeData.Apply();
|
||||
}
|
||||
}
|
||||
62
Penumbra/Collections/Cache/CollectionModData.cs
Normal file
62
Penumbra/Collections/Cache/CollectionModData.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Contains associations between a mod and the paths and meta manipulations affected by that mod.
|
||||
/// </summary>
|
||||
public class CollectionModData
|
||||
{
|
||||
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<IMetaIdentifier>)> _data = new();
|
||||
|
||||
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<IMetaIdentifier>)> Data
|
||||
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<IMetaIdentifier>)kvp.Value.Item2));
|
||||
|
||||
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<IMetaIdentifier> Manipulations) RemoveMod(IMod mod)
|
||||
{
|
||||
if (_data.Remove(mod, out var data))
|
||||
return data;
|
||||
|
||||
return ([], []);
|
||||
}
|
||||
|
||||
public void AddPath(IMod mod, Utf8GamePath path)
|
||||
{
|
||||
if (_data.TryGetValue(mod, out var data))
|
||||
{
|
||||
data.Item1.Add(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = ([path], []);
|
||||
_data.Add(mod, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddManip(IMod mod, IMetaIdentifier manipulation)
|
||||
{
|
||||
if (_data.TryGetValue(mod, out var data))
|
||||
{
|
||||
data.Item2.Add(manipulation);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = ([], [manipulation]);
|
||||
_data.Add(mod, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemovePath(IMod mod, Utf8GamePath path)
|
||||
{
|
||||
if (_data.TryGetValue(mod, out var data) && data.Item1.Remove(path) && data.Item1.Count == 0 && data.Item2.Count == 0)
|
||||
_data.Remove(mod);
|
||||
}
|
||||
|
||||
public void RemoveManip(IMod mod, IMetaIdentifier manip)
|
||||
{
|
||||
if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
|
||||
_data.Remove(mod);
|
||||
}
|
||||
}
|
||||
49
Penumbra/Collections/Cache/CustomResourceCache.cs
Normal file
49
Penumbra/Collections/Cache/CustomResourceCache.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||
using Penumbra.Interop.SafeHandles;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
/// <summary> A cache for resources owned by a collection. </summary>
|
||||
public sealed class CustomResourceCache(ResourceLoader loader)
|
||||
: ConcurrentDictionary<Utf8GamePath, SafeResourceHandle>, IDisposable
|
||||
{
|
||||
/// <summary> Invalidate an existing resource by clearing it from the cache and disposing it. </summary>
|
||||
public void Invalidate(Utf8GamePath path)
|
||||
{
|
||||
if (TryRemove(path, out var handle))
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var handle in Values)
|
||||
handle.Dispose();
|
||||
Clear();
|
||||
}
|
||||
|
||||
/// <summary> Get the requested resource either from the cached resource, or load a new one if it does not exist. </summary>
|
||||
public SafeResourceHandle Get(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData data)
|
||||
{
|
||||
if (TryGetClonedValue(path, out var handle))
|
||||
return handle;
|
||||
|
||||
handle = loader.LoadResolvedSafeResource(category, type, path.Path, data);
|
||||
var clone = handle.Clone();
|
||||
if (!TryAdd(path, clone))
|
||||
clone.Dispose();
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary> Get a cloned cached resource if it exists. </summary>
|
||||
private bool TryGetClonedValue(Utf8GamePath path, [NotNullWhen(true)] out SafeResourceHandle? handle)
|
||||
{
|
||||
if (!TryGetValue(path, out handle))
|
||||
return false;
|
||||
|
||||
handle = handle.Clone();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
54
Penumbra/Collections/Cache/EqdpCache.cs
Normal file
54
Penumbra/Collections/Cache/EqdpCache.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqdpIdentifier, EqdpEntry>(manager, collection)
|
||||
{
|
||||
private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries =
|
||||
[];
|
||||
|
||||
public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry)
|
||||
=> _fullEntries.TryGetValue((id, genderRace, accessory), out var pair)
|
||||
? (originalEntry & pair.InverseMask) | pair.Entry
|
||||
: originalEntry;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Clear();
|
||||
_fullEntries.Clear();
|
||||
}
|
||||
|
||||
protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry)
|
||||
{
|
||||
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
|
||||
var mask = Eqdp.Mask(identifier.Slot);
|
||||
var inverseMask = ~mask;
|
||||
if (_fullEntries.TryGetValue(tuple, out var pair))
|
||||
pair = ((pair.Entry & inverseMask) | (entry & mask), pair.InverseMask & inverseMask);
|
||||
else
|
||||
pair = (entry & mask, inverseMask);
|
||||
_fullEntries[tuple] = pair;
|
||||
}
|
||||
|
||||
protected override void RevertModInternal(EqdpIdentifier identifier)
|
||||
{
|
||||
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
|
||||
|
||||
if (!_fullEntries.Remove(tuple, out var pair))
|
||||
return;
|
||||
|
||||
var mask = Eqdp.Mask(identifier.Slot);
|
||||
var newMask = pair.InverseMask | mask;
|
||||
if (newMask is not EqdpEntry.FullMask)
|
||||
_fullEntries[tuple] = (pair.Entry & ~mask, newMask);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
{
|
||||
Clear();
|
||||
_fullEntries.Clear();
|
||||
}
|
||||
}
|
||||
66
Penumbra/Collections/Cache/EqpCache.cs
Normal file
66
Penumbra/Collections/Cache/EqpCache.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqpIdentifier, EqpEntry>(manager, collection)
|
||||
{
|
||||
public unsafe EqpEntry GetValues(CharacterArmor* armor)
|
||||
{
|
||||
var bodyEntry = GetSingleValue(armor[1].Set, EquipSlot.Body);
|
||||
var headEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHead)
|
||||
? GetSingleValue(armor[0].Set, EquipSlot.Head)
|
||||
: GetSingleValue(armor[1].Set, EquipSlot.Head);
|
||||
var handEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHand)
|
||||
? GetSingleValue(armor[2].Set, EquipSlot.Hands)
|
||||
: GetSingleValue(armor[1].Set, EquipSlot.Hands);
|
||||
var (legsEntry, legsId) = bodyEntry.HasFlag(EqpEntry.BodyShowLeg)
|
||||
? (GetSingleValue(armor[3].Set, EquipSlot.Legs), 3)
|
||||
: (GetSingleValue(armor[1].Set, EquipSlot.Legs), 1);
|
||||
var footEntry = legsEntry.HasFlag(EqpEntry.LegsShowFoot)
|
||||
? GetSingleValue(armor[4].Set, EquipSlot.Feet)
|
||||
: GetSingleValue(armor[legsId].Set, EquipSlot.Feet);
|
||||
|
||||
var combined = bodyEntry | headEntry | handEntry | legsEntry | footEntry;
|
||||
return PostProcessFeet(PostProcessHands(combined));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot)
|
||||
=> TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot);
|
||||
|
||||
public void Reset()
|
||||
=> Clear();
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
=> Clear();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static EqpEntry PostProcessHands(EqpEntry entry)
|
||||
{
|
||||
if (!entry.HasFlag(EqpEntry.HandsHideForearm))
|
||||
return entry;
|
||||
|
||||
var testFlag = entry.HasFlag(EqpEntry.HandsHideElbow)
|
||||
? entry.HasFlag(EqpEntry.BodyHideGlovesL)
|
||||
: entry.HasFlag(EqpEntry.BodyHideGlovesM);
|
||||
return testFlag
|
||||
? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS
|
||||
: entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static EqpEntry PostProcessFeet(EqpEntry entry)
|
||||
{
|
||||
if (!entry.HasFlag(EqpEntry.FeetHideCalf))
|
||||
return entry;
|
||||
|
||||
if (entry.HasFlag(EqpEntry.FeetHideKnee) || !entry.HasFlag(EqpEntry._20))
|
||||
return entry & ~(EqpEntry.LegsHideBootsS | EqpEntry.LegsHideBootsM);
|
||||
|
||||
return (entry | EqpEntry.LegsHideBootsM) & ~EqpEntry.LegsHideBootsS;
|
||||
}
|
||||
}
|
||||
19
Penumbra/Collections/Cache/EstCache.cs
Normal file
19
Penumbra/Collections/Cache/EstCache.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EstIdentifier, EstEntry>(manager, collection)
|
||||
{
|
||||
public EstEntry GetEstEntry(EstIdentifier identifier)
|
||||
=> TryGetValue(identifier, out var entry)
|
||||
? entry.Entry
|
||||
: EstFile.GetDefault(Manager, identifier);
|
||||
|
||||
public void Reset()
|
||||
=> Clear();
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
=> Clear();
|
||||
}
|
||||
122
Penumbra/Collections/Cache/GlobalEqpCache.cs
Normal file
122
Penumbra/Collections/Cache/GlobalEqpCache.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>, IService
|
||||
{
|
||||
private readonly HashSet<PrimaryId> _doNotHideEarrings = [];
|
||||
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
|
||||
private readonly HashSet<PrimaryId> _doNotHideBracelets = [];
|
||||
private readonly HashSet<PrimaryId> _doNotHideRingL = [];
|
||||
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
|
||||
private bool _doNotHideVieraHats;
|
||||
private bool _doNotHideHrothgarHats;
|
||||
private bool _hideAuRaHorns;
|
||||
private bool _hideVieraEars;
|
||||
private bool _hideMiqoteEars;
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
_doNotHideEarrings.Clear();
|
||||
_doNotHideNecklace.Clear();
|
||||
_doNotHideBracelets.Clear();
|
||||
_doNotHideRingL.Clear();
|
||||
_doNotHideRingR.Clear();
|
||||
_doNotHideHrothgarHats = false;
|
||||
_doNotHideVieraHats = false;
|
||||
_hideAuRaHorns = false;
|
||||
_hideVieraEars = false;
|
||||
_hideMiqoteEars = false;
|
||||
}
|
||||
|
||||
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
|
||||
{
|
||||
if (Count == 0)
|
||||
return original;
|
||||
|
||||
if (_doNotHideVieraHats)
|
||||
original |= EqpEntry.HeadShowVieraHat;
|
||||
|
||||
if (_doNotHideHrothgarHats)
|
||||
original |= EqpEntry.HeadShowHrothgarHat;
|
||||
|
||||
if (_hideAuRaHorns)
|
||||
original &= ~EqpEntry.HeadShowEarAuRa;
|
||||
|
||||
if (_hideVieraEars)
|
||||
original &= ~EqpEntry.HeadShowEarViera;
|
||||
|
||||
if (_hideMiqoteEars)
|
||||
original &= ~EqpEntry.HeadShowEarMiqote;
|
||||
|
||||
if (_doNotHideEarrings.Contains(armor[5].Set))
|
||||
original |= EqpEntry.HeadShowEarringsHyurRoe
|
||||
| EqpEntry.HeadShowEarringsLalaElezen
|
||||
| EqpEntry.HeadShowEarringsMiqoHrothViera
|
||||
| EqpEntry.HeadShowEarringsAura;
|
||||
|
||||
if (_doNotHideNecklace.Contains(armor[6].Set))
|
||||
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
||||
|
||||
if (_doNotHideBracelets.Contains(armor[7].Set))
|
||||
original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet;
|
||||
|
||||
if (_doNotHideRingR.Contains(armor[8].Set))
|
||||
original |= EqpEntry.HandShowRingR;
|
||||
|
||||
if (_doNotHideRingL.Contains(armor[9].Set))
|
||||
original |= EqpEntry.HandShowRingL;
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation)
|
||||
{
|
||||
if (Remove(manipulation, out var oldMod) && oldMod == mod)
|
||||
return false;
|
||||
|
||||
this[manipulation] = mod;
|
||||
_ = manipulation.Type switch
|
||||
{
|
||||
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Add(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
|
||||
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
|
||||
GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true),
|
||||
GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true),
|
||||
GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true),
|
||||
_ => false,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod)
|
||||
{
|
||||
if (!Remove(manipulation, out mod))
|
||||
return false;
|
||||
|
||||
_ = manipulation.Type switch
|
||||
{
|
||||
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Remove(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
|
||||
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
|
||||
GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false),
|
||||
GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false),
|
||||
GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false),
|
||||
_ => false,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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