mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2026-01-03 14:23:43 +01:00
Compare commits
821 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ca572d38 | ||
|
|
59c9601a9b | ||
|
|
0d9a0d49ab | ||
|
|
96f825e298 | ||
|
|
62c9152d8c | ||
|
|
98b702d6e6 | ||
|
|
3c68124b29 | ||
|
|
77f3912bf2 | ||
|
|
643c83a6f3 | ||
|
|
5656d88c94 | ||
|
|
cf87184c92 | ||
|
|
598f598e82 | ||
|
|
06c593bbcb | ||
|
|
507e1268ac | ||
|
|
da14548c43 | ||
|
|
5b6517aae8 | ||
|
|
aadcf771e7 | ||
|
|
04fb37d661 | ||
|
|
bf4673a1d9 | ||
|
|
76b214c643 | ||
|
|
434a5a809e | ||
|
|
88fe25f69e | ||
|
|
bef1e39ac3 | ||
|
|
c604d5dbe5 | ||
|
|
a0d912a395 | ||
|
|
a56852f918 | ||
|
|
4228fc1b89 | ||
|
|
e644b8da28 | ||
|
|
ace3a8f755 | ||
|
|
76ed347cbf | ||
|
|
48bef12555 | ||
|
|
c3469a1687 | ||
|
|
d6c36ca4f7 | ||
|
|
44345b9429 | ||
|
|
20914bc064 | ||
|
|
0a9693daea | ||
|
|
8f362c5121 | ||
|
|
0442fb7b60 | ||
|
|
c62c3c4eea | ||
|
|
6a34d41f6a | ||
|
|
c31f6c19a6 | ||
|
|
da1db70635 | ||
|
|
c420b1f180 | ||
|
|
414bd8bee7 | ||
|
|
8e1745d67a | ||
|
|
889f01a724 | ||
|
|
6e62905fa7 | ||
|
|
654787fa0d | ||
|
|
835ba23935 | ||
|
|
389a8781d6 | ||
|
|
3eabe591df | ||
|
|
487d3b9399 | ||
|
|
4d4e4669dd | ||
|
|
fb065549e9 | ||
|
|
2c34154915 | ||
|
|
3704051b0f | ||
|
|
b2b8f2b6eb | ||
|
|
22e6c0655b | ||
|
|
bb2ba0cf11 | ||
|
|
e854386b23 | ||
|
|
49d24df2e7 | ||
|
|
c9b291c2f3 | ||
|
|
65f789880d | ||
|
|
abf998a727 | ||
|
|
4cc191cb25 | ||
|
|
dc431c10a5 | ||
|
|
26862ba78f | ||
|
|
e4374337f2 | ||
|
|
240c889fff | ||
|
|
612cd31c3e | ||
|
|
304b362002 | ||
|
|
8f34f197d0 | ||
|
|
1c97266a93 | ||
|
|
a9caddafd5 | ||
|
|
34bf95dddb | ||
|
|
4761b8f584 | ||
|
|
0d94aae732 | ||
|
|
e83f328cdc | ||
|
|
52fd29c478 | ||
|
|
a8b79993df | ||
|
|
98574558e5 | ||
|
|
557cbf23ce | ||
|
|
56753ae7ba | ||
|
|
b66df624f7 | ||
|
|
be78f1447b | ||
|
|
97e32a3cb7 | ||
|
|
4472920536 | ||
|
|
ac6a726f57 | ||
|
|
00d550f4fe | ||
|
|
0f98fac157 | ||
|
|
c25f0f72db | ||
|
|
72e05e23bc | ||
|
|
2c3bed6ba5 | ||
|
|
d6df9885dc | ||
|
|
e7936500e0 | ||
|
|
4ef4e65d46 | ||
|
|
40b4a8fd7a | ||
|
|
8a1f03c272 | ||
|
|
8ed479eddf | ||
|
|
c0a278ca2c | ||
|
|
2e9a7004c6 | ||
|
|
75c76a92b9 | ||
|
|
282935c6d6 | ||
|
|
d7b189b714 | ||
|
|
e3da3f356c | ||
|
|
66bed4217f | ||
|
|
56bbf6593a | ||
|
|
b8e1e7c384 | ||
|
|
a0d2c39f45 | ||
|
|
07df3186c2 | ||
|
|
5b59e74417 | ||
|
|
b4485f028d | ||
|
|
74674cfa0c | ||
|
|
f192c17c9b | ||
|
|
aa1ac29182 | ||
|
|
081ac6bf8b | ||
|
|
e4b32343ae | ||
|
|
c93370ec92 | ||
|
|
9abd7f2767 | ||
|
|
8a9877bb01 | ||
|
|
c1e1476fa6 | ||
|
|
b1abbb8e77 | ||
|
|
fcb0660def | ||
|
|
a6073e2a42 | ||
|
|
2c87077918 | ||
|
|
4e0a9f62b9 | ||
|
|
39636f5293 | ||
|
|
b53124e708 | ||
|
|
9a684c9ff5 | ||
|
|
c7d1620c1e | ||
|
|
325b54031c | ||
|
|
155a9d6266 | ||
|
|
4f6fb44f79 | ||
|
|
bfce99859f | ||
|
|
6c556d6a61 | ||
|
|
7ed42005dd | ||
|
|
aad978f5f6 | ||
|
|
c0ad4aab51 | ||
|
|
8fe0ac8195 | ||
|
|
46f8818cee | ||
|
|
118f51cc64 | ||
|
|
096d82741d | ||
|
|
b98cb31fd2 | ||
|
|
90813ce030 | ||
|
|
95bc52b2bc | ||
|
|
d79e4b5853 | ||
|
|
a40a6905be | ||
|
|
6b3a64ce14 | ||
|
|
4fca1600ca | ||
|
|
b1e65e6f9d | ||
|
|
381b23fe0c | ||
|
|
782c4446b2 | ||
|
|
76eaa75d04 | ||
|
|
361ed536a3 | ||
|
|
b1d00e9812 | ||
|
|
b0abf865cb | ||
|
|
d398381b52 | ||
|
|
d75d70bee5 | ||
|
|
296f1e90b5 | ||
|
|
9d3dfbbece | ||
|
|
d6d592f099 | ||
|
|
00cb1b6643 | ||
|
|
22babad789 | ||
|
|
18ff905746 | ||
|
|
fd0d761b92 | ||
|
|
750d4f9eca | ||
|
|
25517525c9 | ||
|
|
99a8e1e8c5 | ||
|
|
381b2a1b8b | ||
|
|
3dee511b9a | ||
|
|
e93c3b7bb8 | ||
|
|
773682838e | ||
|
|
c9f00c6369 | ||
|
|
2026069ed3 | ||
|
|
ae093506c1 | ||
|
|
87f1b613f9 | ||
|
|
71e15474b2 | ||
|
|
3fd6108fa1 | ||
|
|
b9e4c144c2 | ||
|
|
6e685b96d1 | ||
|
|
c96f009fb4 | ||
|
|
3112079776 | ||
|
|
7015737d88 | ||
|
|
94f6e870e6 | ||
|
|
528aae7eee | ||
|
|
425c9471fb | ||
|
|
0c8110e15e | ||
|
|
8a7ec45bbf | ||
|
|
c1f84b4303 | ||
|
|
5a9e9513f4 | ||
|
|
9e7679e70f | ||
|
|
e5b2114ac2 | ||
|
|
1168460942 | ||
|
|
f7d6e75a9b | ||
|
|
d56c2db547 | ||
|
|
ab2a3f5bd9 | ||
|
|
4748d1e211 | ||
|
|
5ca151b675 | ||
|
|
67fd65d366 | ||
|
|
45981f2fee | ||
|
|
1df2a46884 | ||
|
|
2c7b7d59be | ||
|
|
d849506ecd | ||
|
|
47f2cfc210 | ||
|
|
5d1c65cce7 | ||
|
|
cf308fc118 | ||
|
|
87016419c5 | ||
|
|
da46705b52 | ||
|
|
9a1cf3d9e6 | ||
|
|
8e0908dbf7 | ||
|
|
1f255a98e6 | ||
|
|
2a067ef60b | ||
|
|
b3afa2067c | ||
|
|
630c4dd894 | ||
|
|
f1fa5c2fa9 | ||
|
|
f6c9ecad60 | ||
|
|
5eb545f62d | ||
|
|
0f127a557d | ||
|
|
611200d311 | ||
|
|
0ed4603ba5 | ||
|
|
439849edfa | ||
|
|
091aadd4a6 | ||
|
|
356b814102 | ||
|
|
cd6a6a462d | ||
|
|
0123fe1fbd | ||
|
|
8b518d3c6f | ||
|
|
d9f9937d41 | ||
|
|
7be283ca30 | ||
|
|
748c324acf | ||
|
|
2916669319 | ||
|
|
30468e0b09 | ||
|
|
70918e5393 | ||
|
|
c43ce9d978 | ||
|
|
2e11481276 | ||
|
|
f1b335e794 | ||
|
|
96dca5dbe7 | ||
|
|
4db0822443 | ||
|
|
cdc67a035c | ||
|
|
8add6e5519 | ||
|
|
ebdfd3f8bc | ||
|
|
1cd8e5fb7e | ||
|
|
1d185e9bfe | ||
|
|
9c57935a87 | ||
|
|
8b609e5f05 | ||
|
|
e1a41b5f3c | ||
|
|
c605d19510 | ||
|
|
8160f420db | ||
|
|
60a1ee728a | ||
|
|
c83ddf054a | ||
|
|
02bfd17794 | ||
|
|
7d490f9cfb | ||
|
|
c541fd62c4 | ||
|
|
2e038350ef | ||
|
|
157a5b150b | ||
|
|
8a87ff16b4 | ||
|
|
3d6d04dde1 | ||
|
|
d675cdc804 | ||
|
|
6475c3c65b | ||
|
|
d57743763b | ||
|
|
9b9e356ad1 | ||
|
|
24452f3c79 | ||
|
|
e41755ed7e | ||
|
|
71e80740f6 | ||
|
|
70cf21cf57 | ||
|
|
ebd3fb3fbf | ||
|
|
56d014e14f | ||
|
|
664b42eee8 | ||
|
|
a116243900 | ||
|
|
33bf5c5392 | ||
|
|
f3eb542940 | ||
|
|
b90e68fbaf | ||
|
|
c951854d5a | ||
|
|
f424857bd3 | ||
|
|
27e9223c81 | ||
|
|
31cd812519 | ||
|
|
c9febe2c74 | ||
|
|
467dc2c22f | ||
|
|
4215e0ad44 | ||
|
|
533c53fd8d | ||
|
|
66ed721105 | ||
|
|
9e09d64c66 | ||
|
|
7f95726cc3 | ||
|
|
22c7e32760 | ||
|
|
40a684ff46 | ||
|
|
9ed8c9517b | ||
|
|
3722df199b | ||
|
|
5464fa3a0f | ||
|
|
525f65e70a | ||
|
|
b44a147fdd | ||
|
|
86d370226a | ||
|
|
f9c7e567b6 | ||
|
|
7605c7cb29 | ||
|
|
0ba9f538ba | ||
|
|
de9d605fb0 | ||
|
|
db34255127 | ||
|
|
60c7debb5e | ||
|
|
c7b9791a6a | ||
|
|
fe028e5ec7 | ||
|
|
2ce8076e9a | ||
|
|
a5998b84ba | ||
|
|
dd217a2475 | ||
|
|
4738830b8a | ||
|
|
c49102959f | ||
|
|
1d974a0c6c | ||
|
|
dee79c121b | ||
|
|
667ff2490d | ||
|
|
87d8876972 | ||
|
|
530166b81a | ||
|
|
6db4a9623b | ||
|
|
11111817d7 | ||
|
|
da944b50cc | ||
|
|
cca2bf645f | ||
|
|
ef96dadba5 | ||
|
|
210bca4c7c | ||
|
|
9d99d936aa | ||
|
|
415ac63767 | ||
|
|
8f53253bae | ||
|
|
d104b794ae | ||
|
|
ea8b23d3fd | ||
|
|
65e33d91ac | ||
|
|
ce02c64655 | ||
|
|
83e1476e7f | ||
|
|
885063d389 | ||
|
|
816e88e0bd | ||
|
|
726eb52e4f | ||
|
|
5da0738147 | ||
|
|
9b5271bca4 | ||
|
|
c8cc375d4b | ||
|
|
be9c3eab40 | ||
|
|
1f1b04bdfe | ||
|
|
f473abb99c | ||
|
|
9a02ba2987 | ||
|
|
03043ba2c9 | ||
|
|
82536c75c9 | ||
|
|
773f526fba | ||
|
|
fc6604fd5a | ||
|
|
a7c5d513d4 | ||
|
|
38a489d7f0 | ||
|
|
76cdacf80f | ||
|
|
3880c1870b | ||
|
|
6a46a410f7 | ||
|
|
5971592217 | ||
|
|
9e06125092 | ||
|
|
3a07acbd05 | ||
|
|
edc54203b5 | ||
|
|
af58a52a59 | ||
|
|
2282f3f87a | ||
|
|
5cd224b164 | ||
|
|
d7074c5791 | ||
|
|
d594082165 | ||
|
|
1c8d01bdd3 | ||
|
|
a1b455d9a5 | ||
|
|
61cb46a298 | ||
|
|
2e52030c31 | ||
|
|
5bca4b9118 | ||
|
|
1cc7c2f0cd | ||
|
|
6115d24775 | ||
|
|
68bffba3cd | ||
|
|
e2ffcea0b2 | ||
|
|
9f04ee7695 | ||
|
|
f69915dcb3 | ||
|
|
34caf29b32 | ||
|
|
3a63a1b22d | ||
|
|
5460ffd0a0 | ||
|
|
e516fd9318 | ||
|
|
ad79d9cc07 | ||
|
|
ab771fd010 | ||
|
|
d8085dc022 | ||
|
|
f6434cbc61 | ||
|
|
9d569266b5 | ||
|
|
6446309bd7 | ||
|
|
d0d518ddc2 | ||
|
|
e87b216541 | ||
|
|
1e0b7fdfce | ||
|
|
1bee5c680b | ||
|
|
029cf12bed | ||
|
|
ff96e51963 | ||
|
|
60302c37cd | ||
|
|
fb2b676ff0 | ||
|
|
d256702005 | ||
|
|
b2bb50b3d9 | ||
|
|
b1c1cf0f99 | ||
|
|
a7f36da3f5 | ||
|
|
94b7ea2d9d | ||
|
|
55f2053fe6 | ||
|
|
a885411a8c | ||
|
|
3ad67f661a | ||
|
|
ed329ec989 | ||
|
|
f669616616 | ||
|
|
2f95a4ea34 | ||
|
|
de9fb1fd9f | ||
|
|
8b10b3fdfb | ||
|
|
dae3fbc901 | ||
|
|
608ab7beb9 | ||
|
|
0320da0fc5 | ||
|
|
124656c22e | ||
|
|
1725dbb583 | ||
|
|
b3818a90df | ||
|
|
e7d5cf02e6 | ||
|
|
3484a29f7a | ||
|
|
a807c90885 | ||
|
|
951c167058 | ||
|
|
25491adbe3 | ||
|
|
51c7fc83dd | ||
|
|
a5f35dbd17 | ||
|
|
c75315f0f7 | ||
|
|
d3824d6a23 | ||
|
|
303001fed0 | ||
|
|
717f9eb393 | ||
|
|
9529963aa2 | ||
|
|
81059411e5 | ||
|
|
7a602d6ec5 | ||
|
|
7caf6cc08a | ||
|
|
c1d9af2dd0 | ||
|
|
3f99d11179 | ||
|
|
52b89a4177 | ||
|
|
5cf6b0eb75 | ||
|
|
205a95b254 | ||
|
|
cd498b539b | ||
|
|
089c7d392d | ||
|
|
6207f3a67f | ||
|
|
dec3598eff | ||
|
|
eb7cc6ffa0 | ||
|
|
960548f7b2 | ||
|
|
0f40906451 | ||
|
|
9a14f725b8 | ||
|
|
0450c4e3f7 | ||
|
|
86fc472144 | ||
|
|
4c32ca6e63 | ||
|
|
67acdd2d13 | ||
|
|
b7d482a24e | ||
|
|
8ce667e7e4 | ||
|
|
9851533143 | ||
|
|
87d51c04ad | ||
|
|
edd55087db | ||
|
|
9c49b66d71 | ||
|
|
448090e8f5 | ||
|
|
794cea72cc | ||
|
|
20983a5605 | ||
|
|
284920b8fe | ||
|
|
13181311ae | ||
|
|
1341c4316c | ||
|
|
93dcd317c1 | ||
|
|
dbd11f6a95 | ||
|
|
e4883b15cc | ||
|
|
91138176e0 | ||
|
|
6efd89e0ab | ||
|
|
2713e6f1f6 | ||
|
|
86c871fa81 | ||
|
|
8fff09f92e | ||
|
|
9d18707cb7 | ||
|
|
e8096f6e00 | ||
|
|
e0447b1ed4 | ||
|
|
78d7aef13f | ||
|
|
552338e5b5 | ||
|
|
e50474f12d | ||
|
|
dfb3ef3d79 | ||
|
|
4900ccb75a | ||
|
|
f949153292 | ||
|
|
efd51b0b5e | ||
|
|
fb64315d51 | ||
|
|
ee78a2a983 | ||
|
|
a722abb141 | ||
|
|
21aa3e8efc | ||
|
|
0268546f63 | ||
|
|
9f276c7674 | ||
|
|
a1b40068e3 | ||
|
|
43d683ac66 | ||
|
|
9a52dddba3 | ||
|
|
e35f2816e2 | ||
|
|
d81197a40f | ||
|
|
10e508b4e7 | ||
|
|
7091fdd808 | ||
|
|
f6e74c06cc | ||
|
|
12fa14e1c6 | ||
|
|
c573feefec | ||
|
|
0d427dcaba | ||
|
|
8375abd6cb | ||
|
|
3d421881f6 | ||
|
|
71d6a658d6 | ||
|
|
73122ad9bf | ||
|
|
9f3f78cf4c | ||
|
|
43c26f8499 | ||
|
|
02fc8452d2 | ||
|
|
eab1352340 | ||
|
|
633a01f176 | ||
|
|
411ab84d80 | ||
|
|
43d5700d72 | ||
|
|
7c8bd514de | ||
|
|
6269777760 | ||
|
|
2d75c24371 | ||
|
|
9750736d57 | ||
|
|
05d261d4a3 | ||
|
|
8dde1689f7 | ||
|
|
6362c79aa2 | ||
|
|
5ddf882077 | ||
|
|
7af0a1d562 | ||
|
|
fdd74c0514 | ||
|
|
78b15c085f | ||
|
|
2c0423d2b5 | ||
|
|
e8907871af | ||
|
|
85d9dea2dd | ||
|
|
c9160b8167 | ||
|
|
c99aa51f8a | ||
|
|
ee426eb29f | ||
|
|
b5bdf52d16 | ||
|
|
fdb9479f2d | ||
|
|
6696d539d3 | ||
|
|
f35c20ed4d | ||
|
|
ce3197cb25 | ||
|
|
9f9a58af2a | ||
|
|
e6bd91319b | ||
|
|
62d89633bb | ||
|
|
93ba44d230 | ||
|
|
bade38f82e | ||
|
|
96fcf9e727 | ||
|
|
bfe50f459d | ||
|
|
64c1f75ee0 | ||
|
|
f07717240f | ||
|
|
4bbb48b7b9 | ||
|
|
a501d97252 | ||
|
|
22e7a71425 | ||
|
|
00c5c06629 | ||
|
|
ed6f32f757 | ||
|
|
436a975a44 | ||
|
|
8496f86b6b | ||
|
|
c5211ab779 | ||
|
|
62aa9cabbe | ||
|
|
7f04cb7c45 | ||
|
|
139508917b | ||
|
|
2a01b328e1 | ||
|
|
1cf0e2f70e | ||
|
|
f2951ca800 | ||
|
|
3e9edf3617 | ||
|
|
e4324bc4ce | ||
|
|
b8dc64eb1d | ||
|
|
ecf6008b71 | ||
|
|
fdee4c4ca8 | ||
|
|
80a6e89aa5 | ||
|
|
d36e4f891b | ||
|
|
53c2a7495f | ||
|
|
d8ce81cdc4 | ||
|
|
e5f62d3ea9 | ||
|
|
5f6e24c34f | ||
|
|
62c1730a71 | ||
|
|
d6575e6e68 | ||
|
|
cdaabc05e9 | ||
|
|
c85598acf4 | ||
|
|
22a8ba3f35 | ||
|
|
0bc9fc872e | ||
|
|
ef2d9ba207 | ||
|
|
e8f6b93610 | ||
|
|
a5509e8ba0 | ||
|
|
59529476eb | ||
|
|
5f74f4b4d9 | ||
|
|
0f7d7caba8 | ||
|
|
a2843c1298 | ||
|
|
fb7a92b72a | ||
|
|
10962cac6c | ||
|
|
a194f88903 | ||
|
|
02dff90dd0 | ||
|
|
3537406eaa | ||
|
|
f5666680ff | ||
|
|
72f1663e28 | ||
|
|
c40853e5b2 | ||
|
|
488bea0e78 | ||
|
|
b4cd5110f2 | ||
|
|
7dfc20ce23 | ||
|
|
7a33daf954 | ||
|
|
ec0f7a2d1e | ||
|
|
088066f68a | ||
|
|
b228658414 | ||
|
|
7653fd22c0 | ||
|
|
0ea6e6dac5 | ||
|
|
e967c17d84 | ||
|
|
836e3e6603 | ||
|
|
28ce293d49 | ||
|
|
dd5463071e | ||
|
|
fc52d44c9c | ||
|
|
346e4890b0 | ||
|
|
73266811a0 | ||
|
|
99181d2fdb | ||
|
|
b5b9289dc2 | ||
|
|
42ac507b86 | ||
|
|
1fefe7366c | ||
|
|
5e37f8d2e8 | ||
|
|
fb7aac5228 | ||
|
|
5cdcb9288e | ||
|
|
d10043a69a | ||
|
|
818bf71032 | ||
|
|
994b7bfb6c | ||
|
|
eea4de63d5 | ||
|
|
502b2439b4 | ||
|
|
962c4e53ad | ||
|
|
cb45221be2 | ||
|
|
5e5ce4d234 | ||
|
|
beff7adec4 | ||
|
|
447e748ed7 | ||
|
|
0e3d3d1839 | ||
|
|
eba27e10fb | ||
|
|
5992b86e4f | ||
|
|
282d6df165 | ||
|
|
2219d9293f | ||
|
|
a4de13f228 | ||
|
|
25ddbb1310 | ||
|
|
b92dc03eb5 | ||
|
|
46fcac6c7d | ||
|
|
fce8b058b0 | ||
|
|
a284d5adc5 | ||
|
|
b6549899e8 | ||
|
|
5fd4a83aa4 | ||
|
|
1ad70541d3 | ||
|
|
70e4833fb5 | ||
|
|
4f9e224494 | ||
|
|
e4fc86ca38 | ||
|
|
96c4ae762e | ||
|
|
2422295e67 | ||
|
|
5ec112d896 | ||
|
|
18683f7d43 | ||
|
|
c7430e59b3 | ||
|
|
1a409d475a | ||
|
|
59131ec191 | ||
|
|
092e0ee30e | ||
|
|
593bb47241 | ||
|
|
d13e3ccbd7 | ||
|
|
805e192d63 | ||
|
|
4c9f362f21 | ||
|
|
cd50950dae | ||
|
|
53c4dfeee5 | ||
|
|
4abae59974 | ||
|
|
42aa4fb4a2 | ||
|
|
8b8f85dd85 | ||
|
|
3b5f89e6a1 | ||
|
|
1ab9d5a2a7 | ||
|
|
51a9de3108 | ||
|
|
ee0bdace30 | ||
|
|
0cb2933a0e | ||
|
|
5a30107d78 | ||
|
|
78a77eb6c4 | ||
|
|
547c40fe52 | ||
|
|
57f4ccaaa5 | ||
|
|
0430d994f7 | ||
|
|
68655cf309 | ||
|
|
d2fd8f839b | ||
|
|
78ec0f950f | ||
|
|
831908475c | ||
|
|
ed0ee6439a | ||
|
|
13bf83b2b7 | ||
|
|
c245b30eaa | ||
|
|
d57d40bd59 | ||
|
|
8d4f71122c | ||
|
|
cfa35b2379 | ||
|
|
34fa1e37c8 | ||
|
|
6a3fb7f599 | ||
|
|
917e80d467 | ||
|
|
c87885bd3b | ||
|
|
a50b63f67e | ||
|
|
ada0c6f479 | ||
|
|
47e222a9a9 | ||
|
|
d45e47378b | ||
|
|
bc69e6d0ad | ||
|
|
50b1b64141 | ||
|
|
8a9fa98706 | ||
|
|
630647b544 | ||
|
|
ed27b1dff4 | ||
|
|
5d0993a9ce | ||
|
|
c06f617e04 | ||
|
|
7b6e037e5f | ||
|
|
a2731b4010 | ||
|
|
bb671e8dd2 | ||
|
|
5ea779a34c | ||
|
|
6158bcb2f9 | ||
|
|
31bff511b8 | ||
|
|
a5c33a6311 | ||
|
|
d62d7e352f | ||
|
|
1a0a0f681f | ||
|
|
9361560350 | ||
|
|
bbf460f5e0 | ||
|
|
6ecf06a671 | ||
|
|
6130bae81d | ||
|
|
c3f2e7d3a1 | ||
|
|
5f28644b56 | ||
|
|
a5c1e66916 | ||
|
|
fca5e83841 | ||
|
|
2e5cdc229d | ||
|
|
2642f9e7bc | ||
|
|
be33823f9d | ||
|
|
ea7806535a | ||
|
|
060e8047ca | ||
|
|
0a3ca24303 | ||
|
|
9b82f856e1 | ||
|
|
c29b6b5e57 | ||
|
|
9395072bee | ||
|
|
22ea1e344e | ||
|
|
6c5c202356 | ||
|
|
24c3a52f6a | ||
|
|
53388739ca | ||
|
|
ff6905d45e | ||
|
|
5faaf5337e | ||
|
|
f4dfe8e89c | ||
|
|
29799094ea | ||
|
|
0c1dd50890 | ||
|
|
4b242bb3cf | ||
|
|
dd5c56de9d | ||
|
|
6fe68c59d1 | ||
|
|
deba61d721 | ||
|
|
1fa9afb9c6 | ||
|
|
a900219ede | ||
|
|
fcca756e20 | ||
|
|
d81a6b7f6f | ||
|
|
44a65f61fb | ||
|
|
4531cdadbe | ||
|
|
ab76d3508b | ||
|
|
aae4141550 | ||
|
|
03a0cb5514 | ||
|
|
987c26a51d | ||
|
|
e9d0e61b4c | ||
|
|
36970e3275 | ||
|
|
648b3d4515 | ||
|
|
3071599d94 | ||
|
|
181f58f75f | ||
|
|
f2ea528316 | ||
|
|
b0a89b7c19 | ||
|
|
a982c0a1c1 | ||
|
|
36d95c37bc | ||
|
|
5b648ea2a0 | ||
|
|
e37f16eb15 | ||
|
|
768354be31 | ||
|
|
4b92eae723 | ||
|
|
a04b7cd1db | ||
|
|
a7b1d45b75 | ||
|
|
6ee1501c09 | ||
|
|
e317b3683f | ||
|
|
dd289e6bd7 | ||
|
|
2cafa4e32b | ||
|
|
69dce5790b | ||
|
|
11ab85545f | ||
|
|
f414eedf7b | ||
|
|
cc09cced61 | ||
|
|
358e33346f | ||
|
|
668d4c033f | ||
|
|
3177e6ca29 | ||
|
|
cd0196ddb4 | ||
|
|
512d0a1a5f | ||
|
|
6f4a7661d7 | ||
|
|
2f1b85a02a | ||
|
|
87a645b2a3 | ||
|
|
cdefe64e4e | ||
|
|
60a53d4bff | ||
|
|
eed11bb67f | ||
|
|
8412c2bc68 | ||
|
|
c79997dba8 | ||
|
|
dd42b7ab7f | ||
|
|
c11bd629da | ||
|
|
cd289247e9 | ||
|
|
294cbd4653 | ||
|
|
cf566932f9 | ||
|
|
e1fc08fce7 | ||
|
|
0a7d800706 | ||
|
|
a373537adf | ||
|
|
2c3f7fb92a | ||
|
|
380e682cb4 | ||
|
|
321c481c7d | ||
|
|
9c3c1bdf59 | ||
|
|
226dbdd4a8 | ||
|
|
0583cc5bfc | ||
|
|
b4b104f919 | ||
|
|
9c8e9f5ead | ||
|
|
f514f79fe9 | ||
|
|
98c793eafc | ||
|
|
7b939c81d0 | ||
|
|
2b30a88bf4 | ||
|
|
ec7a53bee2 | ||
|
|
68327d3563 | ||
|
|
f55d25b088 | ||
|
|
2afa5734f7 | ||
|
|
053998e5e4 | ||
|
|
c4bb24a6ec | ||
|
|
8f60688e44 | ||
|
|
108cfbd828 | ||
|
|
1891957670 | ||
|
|
09b0db977f | ||
|
|
879b8e49a0 | ||
|
|
8ba6a9fa33 | ||
|
|
823a8606d3 | ||
|
|
859c738080 | ||
|
|
36f6c48f7a | ||
|
|
a3583dd5f1 | ||
|
|
eb6e665147 | ||
|
|
4328f5d680 | ||
|
|
6ad9b56239 | ||
|
|
3d23923c52 | ||
|
|
900953c249 | ||
|
|
9f970a1920 | ||
|
|
d1b6ec2159 | ||
|
|
5e22946666 | ||
|
|
53b9aa9387 | ||
|
|
b7cd6dfe2d | ||
|
|
66cb6ffaad | ||
|
|
7716e4cc2a | ||
|
|
d597793772 | ||
|
|
81395f761c | ||
|
|
bdcc6cb4de | ||
|
|
4618e73c25 | ||
|
|
c0f19a24e4 | ||
|
|
30c5cd0bdb | ||
|
|
8b8b14f2b6 | ||
|
|
0e943b5d1d | ||
|
|
27f1fcd422 | ||
|
|
7a94c4ee07 | ||
|
|
909966d411 | ||
|
|
27c41cac49 | ||
|
|
a84a66a344 | ||
|
|
e2ddde81d9 | ||
|
|
1b0c432680 | ||
|
|
ba81d585d4 | ||
|
|
6dbac4e084 | ||
|
|
277b26cc92 | ||
|
|
56303be6ae | ||
|
|
09493984c0 | ||
|
|
4ad2c84fce |
278 changed files with 29546 additions and 11812 deletions
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -9,13 +9,15 @@ jobs:
|
|||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '7.x.x'
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
|
|
@ -37,7 +39,7 @@ jobs:
|
|||
- name: Archive
|
||||
run: Compress-Archive -Path Glamourer/bin/Release/* -DestinationPath Glamourer.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Glamourer/bin/Release/*
|
||||
|
|
|
|||
12
.github/workflows/test_release.yml
vendored
12
.github/workflows/test_release.yml
vendored
|
|
@ -9,13 +9,15 @@ jobs:
|
|||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '7.x.x'
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
|
|
@ -37,7 +39,7 @@ jobs:
|
|||
- name: Archive
|
||||
run: Compress-Archive -Path Glamourer/bin/Debug/* -DestinationPath Glamourer.zip
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
./Glamourer/bin/Debug/*
|
||||
|
|
|
|||
12
.gitmodules
vendored
12
.gitmodules
vendored
|
|
@ -1,16 +1,20 @@
|
|||
[submodule "OtterGui"]
|
||||
path = OtterGui
|
||||
url = git@github.com:Ottermandias/OtterGui.git
|
||||
url = https://github.com/Ottermandias/OtterGui.git
|
||||
branch = main
|
||||
[submodule "Penumbra.GameData"]
|
||||
path = Penumbra.GameData
|
||||
url = git@github.com:Ottermandias/Penumbra.GameData.git
|
||||
url = https://github.com/Ottermandias/Penumbra.GameData.git
|
||||
branch = main
|
||||
[submodule "Penumbra.String"]
|
||||
path = Penumbra.String
|
||||
url = git@github.com:Ottermandias/Penumbra.String.git
|
||||
url = https://github.com/Ottermandias/Penumbra.String.git
|
||||
branch = main
|
||||
[submodule "Penumbra.Api"]
|
||||
path = Penumbra.Api
|
||||
url = git@github.com:Ottermandias/Penumbra.Api.git
|
||||
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||
branch = main
|
||||
[submodule "Glamourer.Api"]
|
||||
path = Glamourer.Api
|
||||
url = https://github.com/Ottermandias/Glamourer.Api.git
|
||||
branch = main
|
||||
|
|
|
|||
1
Glamourer.Api
Submodule
1
Glamourer.Api
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 5b6730d46f17bdd02a441e23e2141576cf7acf53
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
using Lumina.Data;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
// A custom version of CharaMakeParams that is easier to parse.
|
||||
[Sheet("CharaMakeParams")]
|
||||
public class CharaMakeParams : ExcelRow
|
||||
{
|
||||
public const int NumMenus = 28;
|
||||
public const int NumVoices = 12;
|
||||
public const int NumGraphics = 10;
|
||||
public const int MaxNumValues = 100;
|
||||
public const int NumFaces = 8;
|
||||
public const int NumFeatures = 7;
|
||||
public const int NumEquip = 3;
|
||||
|
||||
public enum MenuType
|
||||
{
|
||||
ListSelector = 0,
|
||||
IconSelector = 1,
|
||||
ColorPicker = 2,
|
||||
DoubleColorPicker = 3,
|
||||
IconCheckmark = 4,
|
||||
Percentage = 5,
|
||||
Checkmark = 6, // custom
|
||||
Nothing = 7, // custom
|
||||
List1Selector = 8, // custom, 1-indexed lists
|
||||
}
|
||||
|
||||
public struct Menu
|
||||
{
|
||||
public uint Id;
|
||||
public byte InitVal;
|
||||
public MenuType Type;
|
||||
public byte Size;
|
||||
public byte LookAt;
|
||||
public uint Mask;
|
||||
public uint Customize;
|
||||
public uint[] Values;
|
||||
public byte[] Graphic;
|
||||
}
|
||||
|
||||
public struct FacialFeatures
|
||||
{
|
||||
public uint[] Icons;
|
||||
}
|
||||
|
||||
public LazyRow<Race> Race { get; set; } = null!;
|
||||
public LazyRow<Tribe> Tribe { get; set; } = null!;
|
||||
public sbyte Gender { get; set; }
|
||||
|
||||
public Menu[] Menus { get; set; } = new Menu[NumMenus];
|
||||
public byte[] Voices { get; set; } = new byte[NumVoices];
|
||||
public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces];
|
||||
|
||||
public CharaMakeType.CharaMakeTypeUnkData3347Obj[] Equip { get; set; } = new CharaMakeType.CharaMakeTypeUnkData3347Obj[NumEquip];
|
||||
|
||||
public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language)
|
||||
{
|
||||
RowId = parser.RowId;
|
||||
SubRowId = parser.SubRowId;
|
||||
Race = new LazyRow<Race>(gameData, parser.ReadColumn<uint>(0), language);
|
||||
Tribe = new LazyRow<Tribe>(gameData, parser.ReadColumn<uint>(1), language);
|
||||
Gender = parser.ReadColumn<sbyte>(2);
|
||||
var currentOffset = 0;
|
||||
for (var i = 0; i < NumMenus; ++i)
|
||||
{
|
||||
currentOffset = 3 + i;
|
||||
Menus[i].Id = parser.ReadColumn<uint>(0 * NumMenus + currentOffset);
|
||||
Menus[i].InitVal = parser.ReadColumn<byte>(1 * NumMenus + currentOffset);
|
||||
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(2 * NumMenus + currentOffset);
|
||||
Menus[i].Size = parser.ReadColumn<byte>(3 * NumMenus + currentOffset);
|
||||
Menus[i].LookAt = parser.ReadColumn<byte>(4 * NumMenus + currentOffset);
|
||||
Menus[i].Mask = parser.ReadColumn<uint>(5 * NumMenus + currentOffset);
|
||||
Menus[i].Customize = parser.ReadColumn<uint>(6 * NumMenus + currentOffset);
|
||||
Menus[i].Values = new uint[Menus[i].Size];
|
||||
|
||||
switch (Menus[i].Type)
|
||||
{
|
||||
case MenuType.ColorPicker:
|
||||
case MenuType.DoubleColorPicker:
|
||||
case MenuType.Percentage:
|
||||
break;
|
||||
default:
|
||||
currentOffset += 7 * NumMenus;
|
||||
for (var j = 0; j < Menus[i].Size; ++j)
|
||||
Menus[i].Values[j] = parser.ReadColumn<uint>(j * NumMenus + currentOffset);
|
||||
break;
|
||||
}
|
||||
|
||||
Menus[i].Graphic = new byte[NumGraphics];
|
||||
currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i;
|
||||
for (var j = 0; j < NumGraphics; ++j)
|
||||
Menus[i].Graphic[j] = parser.ReadColumn<byte>(j * NumMenus + currentOffset);
|
||||
}
|
||||
|
||||
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus;
|
||||
for (var i = 0; i < NumVoices; ++i)
|
||||
Voices[i] = parser.ReadColumn<byte>(currentOffset++);
|
||||
|
||||
for (var i = 0; i < NumFaces; ++i)
|
||||
{
|
||||
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i;
|
||||
FacialFeatureByFace[i].Icons = new uint[NumFeatures];
|
||||
for (var j = 0; j < NumFeatures; ++j)
|
||||
FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn<int>(j * NumFaces + currentOffset);
|
||||
}
|
||||
|
||||
for (var i = 0; i < NumEquip; ++i)
|
||||
{
|
||||
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7;
|
||||
Equip[i] = new CharaMakeType.CharaMakeTypeUnkData3347Obj()
|
||||
{
|
||||
Helmet = parser.ReadColumn<ulong>(currentOffset + 0),
|
||||
Top = parser.ReadColumn<ulong>(currentOffset + 1),
|
||||
Gloves = parser.ReadColumn<ulong>(currentOffset + 2),
|
||||
Legs = parser.ReadColumn<ulong>(currentOffset + 3),
|
||||
Shoes = parser.ReadColumn<ulong>(currentOffset + 4),
|
||||
Weapon = parser.ReadColumn<ulong>(currentOffset + 5),
|
||||
SubWeapon = parser.ReadColumn<ulong>(currentOffset + 6),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
// Convert the Human.Cmp file into color sets.
|
||||
// If the file can not be read due to TexTools corruption, create a 0-array of size MinSize.
|
||||
internal class CmpFile
|
||||
{
|
||||
private readonly Lumina.Data.FileResource? _file;
|
||||
private readonly uint[] _rgbaColors;
|
||||
|
||||
// No error checking since only called internally.
|
||||
public IEnumerable<uint> GetSlice(int offset, int count)
|
||||
=> _rgbaColors.Length >= offset + count ? _rgbaColors.Skip(offset).Take(count) : Enumerable.Repeat(0u, count);
|
||||
|
||||
public bool Valid
|
||||
=> _file != null;
|
||||
|
||||
public CmpFile(IDataManager gameData, IPluginLog log)
|
||||
{
|
||||
try
|
||||
{
|
||||
_file = gameData.GetFile("chara/xls/charamake/human.cmp")!;
|
||||
_rgbaColors = new uint[_file.Data.Length >> 2];
|
||||
for (var i = 0; i < _file.Data.Length; i += 4)
|
||||
{
|
||||
_rgbaColors[i >> 2] = _file.Data[i]
|
||||
| (uint)(_file.Data[i + 1] << 8)
|
||||
| (uint)(_file.Data[i + 2] << 16)
|
||||
| (uint)(_file.Data[i + 3] << 24);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
|
||||
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
|
||||
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
|
||||
_file = null;
|
||||
_rgbaColors = Array.Empty<uint>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
namespace Glamourer.Customization;
|
||||
|
||||
// Localization from the game files directly.
|
||||
public enum CustomName
|
||||
{
|
||||
Clan = 0,
|
||||
Gender,
|
||||
Reverse,
|
||||
OddEyes,
|
||||
IrisSmall,
|
||||
IrisLarge,
|
||||
IrisSize,
|
||||
MidlanderM,
|
||||
HighlanderM,
|
||||
WildwoodM,
|
||||
DuskwightM,
|
||||
PlainsfolkM,
|
||||
DunesfolkM,
|
||||
SeekerOfTheSunM,
|
||||
KeeperOfTheMoonM,
|
||||
SeawolfM,
|
||||
HellsguardM,
|
||||
RaenM,
|
||||
XaelaM,
|
||||
HelionM,
|
||||
LostM,
|
||||
RavaM,
|
||||
VeenaM,
|
||||
MidlanderF,
|
||||
HighlanderF,
|
||||
WildwoodF,
|
||||
DuskwightF,
|
||||
PlainsfolkF,
|
||||
DunesfolkF,
|
||||
SeekerOfTheSunF,
|
||||
KeeperOfTheMoonF,
|
||||
SeawolfF,
|
||||
HellsguardF,
|
||||
RaenF,
|
||||
XaelaF,
|
||||
HelionF,
|
||||
LostF,
|
||||
RavaF,
|
||||
VeenaF,
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
public class CustomizationManager : ICustomizationManager
|
||||
{
|
||||
private static CustomizationOptions? _options;
|
||||
|
||||
private CustomizationManager()
|
||||
{ }
|
||||
|
||||
public static ICustomizationManager Create(ITextureProvider textures, IDataManager gameData, IPluginLog log)
|
||||
{
|
||||
_options ??= new CustomizationOptions(textures, gameData, log);
|
||||
return new CustomizationManager();
|
||||
}
|
||||
|
||||
public IReadOnlyList<Race> Races
|
||||
=> CustomizationOptions.Races;
|
||||
|
||||
public IReadOnlyList<SubRace> Clans
|
||||
=> CustomizationOptions.Clans;
|
||||
|
||||
public IReadOnlyList<Gender> Genders
|
||||
=> CustomizationOptions.Genders;
|
||||
|
||||
public CustomizationSet GetList(SubRace clan, Gender gender)
|
||||
=> _options!.GetList(clan, gender);
|
||||
|
||||
public IDalamudTextureWrap GetIcon(uint iconId)
|
||||
=> _options!.GetIcon(iconId);
|
||||
|
||||
public string GetName(CustomName name)
|
||||
=> _options!.GetName(name);
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
public static class CustomizationNpcOptions
|
||||
{
|
||||
public static Dictionary<(SubRace, Gender), IReadOnlyList<(CustomizeIndex, CustomizeValue)>> CreateNpcData(CustomizationSet[] sets,
|
||||
ExcelSheet<BNpcCustomize> bNpc, ExcelSheet<ENpcBase> eNpc)
|
||||
{
|
||||
var customizes = bNpc.SelectWhere(FromBnpcCustomize)
|
||||
.Concat(eNpc.SelectWhere(FromEnpcBase)).ToList();
|
||||
|
||||
var dict = new Dictionary<(SubRace, Gender), HashSet<(CustomizeIndex, CustomizeValue)>>();
|
||||
var customizeIndices = new[]
|
||||
{
|
||||
CustomizeIndex.Face,
|
||||
CustomizeIndex.Hairstyle,
|
||||
CustomizeIndex.LipColor,
|
||||
CustomizeIndex.SkinColor,
|
||||
CustomizeIndex.FacePaintColor,
|
||||
CustomizeIndex.HighlightsColor,
|
||||
CustomizeIndex.HairColor,
|
||||
CustomizeIndex.FacePaint,
|
||||
CustomizeIndex.TattooColor,
|
||||
CustomizeIndex.EyeColorLeft,
|
||||
CustomizeIndex.EyeColorRight,
|
||||
};
|
||||
|
||||
foreach (var customize in customizes)
|
||||
{
|
||||
var set = sets[CustomizationOptions.ToIndex(customize.Clan, customize.Gender)];
|
||||
foreach (var customizeIndex in customizeIndices)
|
||||
{
|
||||
var value = customize[customizeIndex];
|
||||
if (value == CustomizeValue.Zero)
|
||||
continue;
|
||||
|
||||
if (set.DataByValue(customizeIndex, value, out _, customize.Face) >= 0)
|
||||
continue;
|
||||
|
||||
if (!dict.TryGetValue((set.Clan, set.Gender), out var npcSet))
|
||||
{
|
||||
npcSet = new HashSet<(CustomizeIndex, CustomizeValue)> { (customizeIndex, value) };
|
||||
dict.Add((set.Clan, set.Gender), npcSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
npcSet.Add((customizeIndex, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dict.ToDictionary(kvp => kvp.Key,
|
||||
kvp => (IReadOnlyList<(CustomizeIndex, CustomizeValue)>)kvp.Value.OrderBy(p => p.Item1).ThenBy(p => p.Item2.Value).ToArray());
|
||||
}
|
||||
|
||||
private static (bool, Customize) FromBnpcCustomize(BNpcCustomize bnpcCustomize)
|
||||
{
|
||||
var customize = new Customize();
|
||||
customize.Data.Set(0, (byte)bnpcCustomize.Race.Row);
|
||||
customize.Data.Set(1, bnpcCustomize.Gender);
|
||||
customize.Data.Set(2, bnpcCustomize.BodyType);
|
||||
customize.Data.Set(3, bnpcCustomize.Height);
|
||||
customize.Data.Set(4, (byte)bnpcCustomize.Tribe.Row);
|
||||
customize.Data.Set(5, bnpcCustomize.Face);
|
||||
customize.Data.Set(6, bnpcCustomize.HairStyle);
|
||||
customize.Data.Set(7, bnpcCustomize.HairHighlight);
|
||||
customize.Data.Set(8, bnpcCustomize.SkinColor);
|
||||
customize.Data.Set(9, bnpcCustomize.EyeHeterochromia);
|
||||
customize.Data.Set(10, bnpcCustomize.HairColor);
|
||||
customize.Data.Set(11, bnpcCustomize.HairHighlightColor);
|
||||
customize.Data.Set(12, bnpcCustomize.FacialFeature);
|
||||
customize.Data.Set(13, bnpcCustomize.FacialFeatureColor);
|
||||
customize.Data.Set(14, bnpcCustomize.Eyebrows);
|
||||
customize.Data.Set(15, bnpcCustomize.EyeColor);
|
||||
customize.Data.Set(16, bnpcCustomize.EyeShape);
|
||||
customize.Data.Set(17, bnpcCustomize.Nose);
|
||||
customize.Data.Set(18, bnpcCustomize.Jaw);
|
||||
customize.Data.Set(19, bnpcCustomize.Mouth);
|
||||
customize.Data.Set(20, bnpcCustomize.LipColor);
|
||||
customize.Data.Set(21, bnpcCustomize.BustOrTone1);
|
||||
customize.Data.Set(22, bnpcCustomize.ExtraFeature1);
|
||||
customize.Data.Set(23, bnpcCustomize.ExtraFeature2OrBust);
|
||||
customize.Data.Set(24, bnpcCustomize.FacePaint);
|
||||
customize.Data.Set(25, bnpcCustomize.FacePaintColor);
|
||||
|
||||
if (customize.BodyType.Value != 1
|
||||
|| !CustomizationOptions.Races.Contains(customize.Race)
|
||||
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|
||||
|| !CustomizationOptions.Genders.Contains(customize.Gender))
|
||||
return (false, Customize.Default);
|
||||
|
||||
return (true, customize);
|
||||
}
|
||||
|
||||
private static (bool, Customize) FromEnpcBase(ENpcBase enpcBase)
|
||||
{
|
||||
if (enpcBase.ModelChara.Value?.Type != 1)
|
||||
return (false, Customize.Default);
|
||||
|
||||
var customize = new Customize();
|
||||
customize.Data.Set(0, (byte)enpcBase.Race.Row);
|
||||
customize.Data.Set(1, enpcBase.Gender);
|
||||
customize.Data.Set(2, enpcBase.BodyType);
|
||||
customize.Data.Set(3, enpcBase.Height);
|
||||
customize.Data.Set(4, (byte)enpcBase.Tribe.Row);
|
||||
customize.Data.Set(5, enpcBase.Face);
|
||||
customize.Data.Set(6, enpcBase.HairStyle);
|
||||
customize.Data.Set(7, enpcBase.HairHighlight);
|
||||
customize.Data.Set(8, enpcBase.SkinColor);
|
||||
customize.Data.Set(9, enpcBase.EyeHeterochromia);
|
||||
customize.Data.Set(10, enpcBase.HairColor);
|
||||
customize.Data.Set(11, enpcBase.HairHighlightColor);
|
||||
customize.Data.Set(12, enpcBase.FacialFeature);
|
||||
customize.Data.Set(13, enpcBase.FacialFeatureColor);
|
||||
customize.Data.Set(14, enpcBase.Eyebrows);
|
||||
customize.Data.Set(15, enpcBase.EyeColor);
|
||||
customize.Data.Set(16, enpcBase.EyeShape);
|
||||
customize.Data.Set(17, enpcBase.Nose);
|
||||
customize.Data.Set(18, enpcBase.Jaw);
|
||||
customize.Data.Set(19, enpcBase.Mouth);
|
||||
customize.Data.Set(20, enpcBase.LipColor);
|
||||
customize.Data.Set(21, enpcBase.BustOrTone1);
|
||||
customize.Data.Set(22, enpcBase.ExtraFeature1);
|
||||
customize.Data.Set(23, enpcBase.ExtraFeature2OrBust);
|
||||
customize.Data.Set(24, enpcBase.FacePaint);
|
||||
customize.Data.Set(25, enpcBase.FacePaintColor);
|
||||
|
||||
if (customize.BodyType.Value != 1
|
||||
|| !CustomizationOptions.Races.Contains(customize.Race)
|
||||
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|
||||
|| !CustomizationOptions.Genders.Contains(customize.Gender))
|
||||
return (false, Customize.Default);
|
||||
|
||||
return (true, customize);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,527 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Dalamud;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Race = Penumbra.GameData.Enums.Race;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
// Generate everything about customization per tribe and gender.
|
||||
public partial class CustomizationOptions
|
||||
{
|
||||
// All races except for Unknown
|
||||
internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
|
||||
|
||||
// All tribes except for Unknown
|
||||
internal static readonly SubRace[] Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
|
||||
|
||||
// Two genders.
|
||||
internal static readonly Gender[] Genders =
|
||||
{
|
||||
Gender.Male,
|
||||
Gender.Female,
|
||||
};
|
||||
|
||||
// Every tribe and gender has a separate set of available customizations.
|
||||
internal CustomizationSet GetList(SubRace race, Gender gender)
|
||||
=> _customizationSets[ToIndex(race, gender)];
|
||||
|
||||
// Get specific icons.
|
||||
internal IDalamudTextureWrap GetIcon(uint id)
|
||||
=> _icons.LoadIcon(id)!;
|
||||
|
||||
private readonly IconStorage _icons;
|
||||
|
||||
private static readonly int ListSize = Clans.Length * Genders.Length;
|
||||
private readonly CustomizationSet[] _customizationSets = new CustomizationSet[ListSize];
|
||||
|
||||
|
||||
// Get the index for the given pair of tribe and gender.
|
||||
internal static int ToIndex(SubRace race, Gender gender)
|
||||
{
|
||||
var idx = ((int)race - 1) * Genders.Length + (gender == Gender.Female ? 1 : 0);
|
||||
if (idx < 0 || idx >= ListSize)
|
||||
ThrowException(race, gender);
|
||||
return idx;
|
||||
}
|
||||
|
||||
private static void ThrowException(SubRace race, Gender gender)
|
||||
=> throw new Exception($"Invalid customization requested for {race} {gender}.");
|
||||
}
|
||||
|
||||
public partial class CustomizationOptions
|
||||
{
|
||||
public string GetName(CustomName name)
|
||||
=> _names[(int)name];
|
||||
|
||||
internal CustomizationOptions(ITextureProvider textures, IDataManager gameData, IPluginLog log)
|
||||
{
|
||||
var tmp = new TemporaryData(gameData, this, log);
|
||||
_icons = new IconStorage(textures, gameData);
|
||||
SetNames(gameData, tmp);
|
||||
foreach (var race in Clans)
|
||||
{
|
||||
foreach (var gender in Genders)
|
||||
_customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender);
|
||||
}
|
||||
tmp.SetNpcData(_customizationSets);
|
||||
}
|
||||
|
||||
// Obtain localized names of customization options and race names from the game data.
|
||||
private readonly string[] _names = new string[Enum.GetValues<CustomName>().Length];
|
||||
|
||||
private void SetNames(IDataManager gameData, TemporaryData tmp)
|
||||
{
|
||||
var subRace = gameData.GetExcelSheet<Tribe>()!;
|
||||
|
||||
void Set(CustomName id, Lumina.Text.SeString? s, string def)
|
||||
=> _names[(int)id] = s?.ToDalamudString().TextValue ?? def;
|
||||
|
||||
Set(CustomName.Clan, tmp.Lobby.GetRow(102)?.Text, "Clan");
|
||||
Set(CustomName.Gender, tmp.Lobby.GetRow(103)?.Text, "Gender");
|
||||
Set(CustomName.Reverse, tmp.Lobby.GetRow(2135)?.Text, "Reverse");
|
||||
Set(CustomName.OddEyes, tmp.Lobby.GetRow(2125)?.Text, "Odd Eyes");
|
||||
Set(CustomName.IrisSmall, tmp.Lobby.GetRow(1076)?.Text, "Small");
|
||||
Set(CustomName.IrisLarge, tmp.Lobby.GetRow(1075)?.Text, "Large");
|
||||
Set(CustomName.IrisSize, tmp.Lobby.GetRow(244)?.Text, "Iris Size");
|
||||
Set(CustomName.MidlanderM, subRace.GetRow((int)SubRace.Midlander)?.Masculine, SubRace.Midlander.ToName());
|
||||
Set(CustomName.MidlanderF, subRace.GetRow((int)SubRace.Midlander)?.Feminine, SubRace.Midlander.ToName());
|
||||
Set(CustomName.HighlanderM, subRace.GetRow((int)SubRace.Highlander)?.Masculine, SubRace.Highlander.ToName());
|
||||
Set(CustomName.HighlanderF, subRace.GetRow((int)SubRace.Highlander)?.Feminine, SubRace.Highlander.ToName());
|
||||
Set(CustomName.WildwoodM, subRace.GetRow((int)SubRace.Wildwood)?.Masculine, SubRace.Wildwood.ToName());
|
||||
Set(CustomName.WildwoodF, subRace.GetRow((int)SubRace.Wildwood)?.Feminine, SubRace.Wildwood.ToName());
|
||||
Set(CustomName.DuskwightM, subRace.GetRow((int)SubRace.Duskwight)?.Masculine, SubRace.Duskwight.ToName());
|
||||
Set(CustomName.DuskwightF, subRace.GetRow((int)SubRace.Duskwight)?.Feminine, SubRace.Duskwight.ToName());
|
||||
Set(CustomName.PlainsfolkM, subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine, SubRace.Plainsfolk.ToName());
|
||||
Set(CustomName.PlainsfolkF, subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine, SubRace.Plainsfolk.ToName());
|
||||
Set(CustomName.DunesfolkM, subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine, SubRace.Dunesfolk.ToName());
|
||||
Set(CustomName.DunesfolkF, subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine, SubRace.Dunesfolk.ToName());
|
||||
Set(CustomName.SeekerOfTheSunM, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine, SubRace.SeekerOfTheSun.ToName());
|
||||
Set(CustomName.SeekerOfTheSunF, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine, SubRace.SeekerOfTheSun.ToName());
|
||||
Set(CustomName.KeeperOfTheMoonM, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine, SubRace.KeeperOfTheMoon.ToName());
|
||||
Set(CustomName.KeeperOfTheMoonF, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine, SubRace.KeeperOfTheMoon.ToName());
|
||||
Set(CustomName.SeawolfM, subRace.GetRow((int)SubRace.Seawolf)?.Masculine, SubRace.Seawolf.ToName());
|
||||
Set(CustomName.SeawolfF, subRace.GetRow((int)SubRace.Seawolf)?.Feminine, SubRace.Seawolf.ToName());
|
||||
Set(CustomName.HellsguardM, subRace.GetRow((int)SubRace.Hellsguard)?.Masculine, SubRace.Hellsguard.ToName());
|
||||
Set(CustomName.HellsguardF, subRace.GetRow((int)SubRace.Hellsguard)?.Feminine, SubRace.Hellsguard.ToName());
|
||||
Set(CustomName.RaenM, subRace.GetRow((int)SubRace.Raen)?.Masculine, SubRace.Raen.ToName());
|
||||
Set(CustomName.RaenF, subRace.GetRow((int)SubRace.Raen)?.Feminine, SubRace.Raen.ToName());
|
||||
Set(CustomName.XaelaM, subRace.GetRow((int)SubRace.Xaela)?.Masculine, SubRace.Xaela.ToName());
|
||||
Set(CustomName.XaelaF, subRace.GetRow((int)SubRace.Xaela)?.Feminine, SubRace.Xaela.ToName());
|
||||
Set(CustomName.HelionM, subRace.GetRow((int)SubRace.Helion)?.Masculine, SubRace.Helion.ToName());
|
||||
Set(CustomName.HelionF, subRace.GetRow((int)SubRace.Helion)?.Feminine, SubRace.Helion.ToName());
|
||||
Set(CustomName.LostM, subRace.GetRow((int)SubRace.Lost)?.Masculine, SubRace.Lost.ToName());
|
||||
Set(CustomName.LostF, subRace.GetRow((int)SubRace.Lost)?.Feminine, SubRace.Lost.ToName());
|
||||
Set(CustomName.RavaM, subRace.GetRow((int)SubRace.Rava)?.Masculine, SubRace.Rava.ToName());
|
||||
Set(CustomName.RavaF, subRace.GetRow((int)SubRace.Rava)?.Feminine, SubRace.Rava.ToName());
|
||||
Set(CustomName.VeenaM, subRace.GetRow((int)SubRace.Veena)?.Masculine, SubRace.Veena.ToName());
|
||||
Set(CustomName.VeenaF, subRace.GetRow((int)SubRace.Veena)?.Feminine, SubRace.Veena.ToName());
|
||||
}
|
||||
|
||||
private class TemporaryData
|
||||
{
|
||||
public bool Valid
|
||||
=> _cmpFile.Valid;
|
||||
|
||||
public CustomizationSet GetSet(SubRace race, Gender gender)
|
||||
{
|
||||
var (skin, hair) = GetColors(race, gender);
|
||||
var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
|
||||
var hrothgar = race.ToRace() == Race.Hrothgar;
|
||||
// Create the initial set with all the easily accessible parameters available for anyone.
|
||||
var set = new CustomizationSet(race, gender)
|
||||
{
|
||||
Voices = row.Voices,
|
||||
HairStyles = GetHairStyles(race, gender),
|
||||
HairColors = hair,
|
||||
SkinColors = skin,
|
||||
EyeColors = _eyeColorPicker,
|
||||
HighlightColors = _highlightPicker,
|
||||
TattooColors = _tattooColorPicker,
|
||||
LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
|
||||
LipColorsLight = hrothgar ? Array.Empty<CustomizeData>() : _lipColorPickerLight,
|
||||
FacePaintColorsDark = _facePaintColorPickerDark,
|
||||
FacePaintColorsLight = _facePaintColorPickerLight,
|
||||
Faces = GetFaces(row),
|
||||
NumEyebrows = GetListSize(row, CustomizeIndex.Eyebrows),
|
||||
NumEyeShapes = GetListSize(row, CustomizeIndex.EyeShape),
|
||||
NumNoseShapes = GetListSize(row, CustomizeIndex.Nose),
|
||||
NumJawShapes = GetListSize(row, CustomizeIndex.Jaw),
|
||||
NumMouthShapes = GetListSize(row, CustomizeIndex.Mouth),
|
||||
FacePaints = GetFacePaints(race, gender),
|
||||
TailEarShapes = GetTailEarShapes(row),
|
||||
};
|
||||
|
||||
SetAvailability(set, row);
|
||||
SetFacialFeatures(set, row);
|
||||
SetHairByFace(set);
|
||||
SetMenuTypes(set, row);
|
||||
SetNames(set, row);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
public void SetNpcData(CustomizationSet[] sets)
|
||||
{
|
||||
var data = CustomizationNpcOptions.CreateNpcData(sets, _bnpcCustomize, _enpcBase);
|
||||
foreach (var set in sets)
|
||||
{
|
||||
if (data.TryGetValue((set.Clan, set.Gender), out var npcData))
|
||||
set.NpcOptions = npcData.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public TemporaryData(IDataManager gameData, CustomizationOptions options, IPluginLog log)
|
||||
{
|
||||
_options = options;
|
||||
_cmpFile = new CmpFile(gameData, log);
|
||||
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>()!;
|
||||
_bnpcCustomize = gameData.GetExcelSheet<BNpcCustomize>()!;
|
||||
_enpcBase = gameData.GetExcelSheet<ENpcBase>()!;
|
||||
Lobby = gameData.GetExcelSheet<Lobby>()!;
|
||||
var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
|
||||
.MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[]
|
||||
{
|
||||
"charamaketype",
|
||||
gameData.Language.ToLumina(),
|
||||
null,
|
||||
}) as ExcelSheet<CharaMakeParams>;
|
||||
_listSheet = tmp!;
|
||||
_hairSheet = gameData.GetExcelSheet<HairMakeType>()!;
|
||||
_highlightPicker = CreateColorPicker(CustomizeIndex.HighlightsColor, 256, 192);
|
||||
_lipColorPickerDark = CreateColorPicker(CustomizeIndex.LipColor, 512, 96);
|
||||
_lipColorPickerLight = CreateColorPicker(CustomizeIndex.LipColor, 1024, 96, true);
|
||||
_eyeColorPicker = CreateColorPicker(CustomizeIndex.EyeColorLeft, 0, 192);
|
||||
_facePaintColorPickerDark = CreateColorPicker(CustomizeIndex.FacePaintColor, 640, 96);
|
||||
_facePaintColorPickerLight = CreateColorPicker(CustomizeIndex.FacePaintColor, 1152, 96, true);
|
||||
_tattooColorPicker = CreateColorPicker(CustomizeIndex.TattooColor, 0, 192);
|
||||
}
|
||||
|
||||
// Required sheets.
|
||||
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
|
||||
private readonly ExcelSheet<CharaMakeParams> _listSheet;
|
||||
private readonly ExcelSheet<HairMakeType> _hairSheet;
|
||||
private readonly ExcelSheet<BNpcCustomize> _bnpcCustomize;
|
||||
private readonly ExcelSheet<ENpcBase> _enpcBase;
|
||||
public readonly ExcelSheet<Lobby> Lobby;
|
||||
private readonly CmpFile _cmpFile;
|
||||
|
||||
// Those values are shared between all races.
|
||||
private readonly CustomizeData[] _highlightPicker;
|
||||
private readonly CustomizeData[] _eyeColorPicker;
|
||||
private readonly CustomizeData[] _facePaintColorPickerDark;
|
||||
private readonly CustomizeData[] _facePaintColorPickerLight;
|
||||
private readonly CustomizeData[] _lipColorPickerDark;
|
||||
private readonly CustomizeData[] _lipColorPickerLight;
|
||||
private readonly CustomizeData[] _tattooColorPicker;
|
||||
|
||||
private readonly CustomizationOptions _options;
|
||||
|
||||
|
||||
private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false)
|
||||
=> _cmpFile.GetSlice(offset, num)
|
||||
.Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
|
||||
.ToArray();
|
||||
|
||||
|
||||
private void SetHairByFace(CustomizationSet set)
|
||||
{
|
||||
if (set.Race != Race.Hrothgar)
|
||||
{
|
||||
set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray();
|
||||
return;
|
||||
}
|
||||
|
||||
var tmp = new IReadOnlyList<CustomizeData>[set.Faces.Count + 1];
|
||||
tmp[0] = set.HairStyles;
|
||||
|
||||
for (var i = 1; i <= set.Faces.Count; ++i)
|
||||
{
|
||||
bool Valid(CustomizeData c)
|
||||
{
|
||||
var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0;
|
||||
return data == 0 || data == i + set.Faces.Count;
|
||||
}
|
||||
|
||||
tmp[i] = set.HairStyles.Where(Valid).ToArray();
|
||||
}
|
||||
|
||||
set.HairByFace = tmp;
|
||||
}
|
||||
|
||||
private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row)
|
||||
{
|
||||
// Set up the menu types for all customizations.
|
||||
set.Types = Enum.GetValues<CustomizeIndex>().Select(c =>
|
||||
{
|
||||
// Those types are not correctly given in the menu, so special case them to color pickers.
|
||||
switch (c)
|
||||
{
|
||||
case CustomizeIndex.HighlightsColor:
|
||||
case CustomizeIndex.EyeColorLeft:
|
||||
case CustomizeIndex.EyeColorRight:
|
||||
case CustomizeIndex.FacePaintColor:
|
||||
return CharaMakeParams.MenuType.ColorPicker;
|
||||
case CustomizeIndex.BodyType: return CharaMakeParams.MenuType.Nothing;
|
||||
case CustomizeIndex.FacePaintReversed:
|
||||
case CustomizeIndex.Highlights:
|
||||
case CustomizeIndex.SmallIris:
|
||||
case CustomizeIndex.Lipstick:
|
||||
return CharaMakeParams.MenuType.Checkmark;
|
||||
case CustomizeIndex.FacialFeature1:
|
||||
case CustomizeIndex.FacialFeature2:
|
||||
case CustomizeIndex.FacialFeature3:
|
||||
case CustomizeIndex.FacialFeature4:
|
||||
case CustomizeIndex.FacialFeature5:
|
||||
case CustomizeIndex.FacialFeature6:
|
||||
case CustomizeIndex.FacialFeature7:
|
||||
case CustomizeIndex.LegacyTattoo:
|
||||
return CharaMakeParams.MenuType.IconCheckmark;
|
||||
}
|
||||
|
||||
var gameId = c.ToByteAndMask().ByteIdx;
|
||||
// Otherwise find the first menu corresponding to the id.
|
||||
// If there is none, assume a list.
|
||||
var menu = row.Menus
|
||||
.Cast<CharaMakeParams.Menu?>()
|
||||
.FirstOrDefault(m => m!.Value.Customize == gameId);
|
||||
var ret = menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
|
||||
if (c is CustomizeIndex.TailShape && ret is CharaMakeParams.MenuType.ListSelector)
|
||||
ret = CharaMakeParams.MenuType.List1Selector;
|
||||
return ret;
|
||||
}).ToArray();
|
||||
set.Order = CustomizationSet.ComputeOrder(set);
|
||||
}
|
||||
|
||||
// Set customizations available if they have any options.
|
||||
private static void SetAvailability(CustomizationSet set, CharaMakeParams row)
|
||||
{
|
||||
if (set.Race == Race.Hrothgar && set.Gender == Gender.Female)
|
||||
return;
|
||||
|
||||
void Set(bool available, CustomizeIndex flag)
|
||||
{
|
||||
if (available)
|
||||
set.SetAvailable(flag);
|
||||
}
|
||||
|
||||
Set(true, CustomizeIndex.Height);
|
||||
Set(set.Faces.Count > 0, CustomizeIndex.Face);
|
||||
Set(true, CustomizeIndex.Hairstyle);
|
||||
Set(true, CustomizeIndex.Highlights);
|
||||
Set(true, CustomizeIndex.SkinColor);
|
||||
Set(true, CustomizeIndex.EyeColorRight);
|
||||
Set(true, CustomizeIndex.HairColor);
|
||||
Set(true, CustomizeIndex.HighlightsColor);
|
||||
Set(true, CustomizeIndex.TattooColor);
|
||||
Set(set.NumEyebrows > 0, CustomizeIndex.Eyebrows);
|
||||
Set(true, CustomizeIndex.EyeColorLeft);
|
||||
Set(set.NumEyeShapes > 0, CustomizeIndex.EyeShape);
|
||||
Set(set.NumNoseShapes > 0, CustomizeIndex.Nose);
|
||||
Set(set.NumJawShapes > 0, CustomizeIndex.Jaw);
|
||||
Set(set.NumMouthShapes > 0, CustomizeIndex.Mouth);
|
||||
Set(set.LipColorsDark.Count > 0, CustomizeIndex.LipColor);
|
||||
Set(GetListSize(row, CustomizeIndex.MuscleMass) > 0, CustomizeIndex.MuscleMass);
|
||||
Set(set.TailEarShapes.Count > 0, CustomizeIndex.TailShape);
|
||||
Set(GetListSize(row, CustomizeIndex.BustSize) > 0, CustomizeIndex.BustSize);
|
||||
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaint);
|
||||
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintColor);
|
||||
Set(true, CustomizeIndex.FacialFeature1);
|
||||
Set(true, CustomizeIndex.FacialFeature2);
|
||||
Set(true, CustomizeIndex.FacialFeature3);
|
||||
Set(true, CustomizeIndex.FacialFeature4);
|
||||
Set(true, CustomizeIndex.FacialFeature5);
|
||||
Set(true, CustomizeIndex.FacialFeature6);
|
||||
Set(true, CustomizeIndex.FacialFeature7);
|
||||
Set(true, CustomizeIndex.LegacyTattoo);
|
||||
Set(true, CustomizeIndex.SmallIris);
|
||||
Set(set.Race != Race.Hrothgar, CustomizeIndex.Lipstick);
|
||||
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintReversed);
|
||||
}
|
||||
|
||||
// Create a list of lists of facial features and the legacy tattoo.
|
||||
private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row)
|
||||
{
|
||||
var count = set.Faces.Count;
|
||||
set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count);
|
||||
|
||||
static (CustomizeData, CustomizeData) Create(CustomizeIndex i, uint data)
|
||||
=> (new CustomizeData(i, CustomizeValue.Zero, data, 0), new CustomizeData(i, CustomizeValue.Max, data, 1));
|
||||
|
||||
set.LegacyTattoo = Create(CustomizeIndex.LegacyTattoo, 137905);
|
||||
|
||||
var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray();
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var data = row.FacialFeatureByFace[i].Icons;
|
||||
tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]);
|
||||
tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]);
|
||||
tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]);
|
||||
tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]);
|
||||
tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]);
|
||||
tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]);
|
||||
tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]);
|
||||
}
|
||||
|
||||
set.FacialFeature1 = tmp[0];
|
||||
set.FacialFeature2 = tmp[1];
|
||||
set.FacialFeature3 = tmp[2];
|
||||
set.FacialFeature4 = tmp[3];
|
||||
set.FacialFeature5 = tmp[4];
|
||||
set.FacialFeature6 = tmp[5];
|
||||
set.FacialFeature7 = tmp[6];
|
||||
}
|
||||
|
||||
// Set the names for the given set of parameters.
|
||||
private void SetNames(CustomizationSet set, CharaMakeParams row)
|
||||
{
|
||||
var nameArray = Enum.GetValues<CustomizeIndex>().Select(c =>
|
||||
{
|
||||
// Find the first menu that corresponds to the Id.
|
||||
var byteId = c.ToByteAndMask().ByteIdx;
|
||||
var menu = row.Menus
|
||||
.Cast<CharaMakeParams.Menu?>()
|
||||
.FirstOrDefault(m => m!.Value.Customize == byteId);
|
||||
if (menu == null)
|
||||
{
|
||||
// If none exists and the id corresponds to highlights, set the Highlights name.
|
||||
if (c == CustomizeIndex.Highlights)
|
||||
return Lobby.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights";
|
||||
|
||||
// Otherwise there is an error and we use the default name.
|
||||
return c.ToDefaultName();
|
||||
}
|
||||
|
||||
// Facial Features and Tattoos is created by combining two strings.
|
||||
if (c is >= CustomizeIndex.FacialFeature1 and <= CustomizeIndex.LegacyTattoo)
|
||||
return
|
||||
$"{Lobby.GetRow(1741)?.Text.ToDalamudString().ToString() ?? "Facial Features"} & {Lobby.GetRow(1742)?.Text.ToDalamudString().ToString() ?? "Tattoos"}";
|
||||
|
||||
// Otherwise all is normal, get the menu name or if it does not work the default name.
|
||||
var textRow = Lobby.GetRow(menu.Value.Id);
|
||||
return textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName();
|
||||
}).ToArray();
|
||||
|
||||
// Add names for both eye colors.
|
||||
nameArray[(int)CustomizeIndex.EyeColorLeft] = nameArray[(int)CustomizeIndex.EyeColorRight];
|
||||
nameArray[(int)CustomizeIndex.EyeColorRight] = _options.GetName(CustomName.OddEyes);
|
||||
|
||||
set.OptionName = nameArray;
|
||||
}
|
||||
|
||||
// Obtain available skin and hair colors for the given subrace and gender.
|
||||
private (CustomizeData[], CustomizeData[]) GetColors(SubRace race, Gender gender)
|
||||
{
|
||||
if (race is > SubRace.Veena or SubRace.Unknown)
|
||||
throw new ArgumentOutOfRangeException(nameof(race), race, null);
|
||||
|
||||
var gv = gender == Gender.Male ? 0 : 1;
|
||||
var idx = ((int)race * 2 + gv) * 5 + 3;
|
||||
|
||||
return (CreateColorPicker(CustomizeIndex.SkinColor, idx << 8, 192),
|
||||
CreateColorPicker(CustomizeIndex.HairColor, (idx + 1) << 8, 192));
|
||||
}
|
||||
|
||||
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
|
||||
private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
|
||||
{
|
||||
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
|
||||
// Unknown30 is the number of available hairstyles.
|
||||
var hairList = new List<CustomizeData>(row.Unknown30);
|
||||
// Hairstyles can be found starting at Unknown66.
|
||||
for (var i = 0; i < row.Unknown30; ++i)
|
||||
{
|
||||
var name = $"Unknown{66 + i * 9}";
|
||||
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
|
||||
?? uint.MaxValue;
|
||||
if (customizeIdx == uint.MaxValue)
|
||||
continue;
|
||||
|
||||
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
|
||||
var hairRow = _customizeSheet.GetRow(customizeIdx);
|
||||
if (hairRow == null)
|
||||
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx));
|
||||
else if (_options._icons.IconExists(hairRow.Icon))
|
||||
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon,
|
||||
(ushort)hairRow.RowId));
|
||||
}
|
||||
|
||||
return hairList.OrderBy(h => h.Value.Value).ToArray();
|
||||
}
|
||||
|
||||
// Get Features.
|
||||
private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index)
|
||||
{
|
||||
var row = _customizeSheet.GetRow(value);
|
||||
return row == null
|
||||
? new CustomizeData(id, (CustomizeValue)(index + 1), value)
|
||||
: new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId);
|
||||
}
|
||||
|
||||
// Get List sizes.
|
||||
private static int GetListSize(CharaMakeParams row, CustomizeIndex index)
|
||||
{
|
||||
var gameId = index.ToByteAndMask().ByteIdx;
|
||||
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == gameId);
|
||||
return menu?.Size ?? 0;
|
||||
}
|
||||
|
||||
// Get face paints from the hair sheet via reflection.
|
||||
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
|
||||
{
|
||||
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
|
||||
var paintList = new List<CustomizeData>(row.Unknown37);
|
||||
// Number of available face paints is at Unknown37.
|
||||
for (var i = 0; i < row.Unknown37; ++i)
|
||||
{
|
||||
// Face paints start at Unknown73.
|
||||
var name = $"Unknown{73 + i * 9}";
|
||||
var customizeIdx =
|
||||
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
|
||||
?? uint.MaxValue;
|
||||
if (customizeIdx == uint.MaxValue)
|
||||
continue;
|
||||
|
||||
var paintRow = _customizeSheet.GetRow(customizeIdx);
|
||||
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
|
||||
if (paintRow != null)
|
||||
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
|
||||
(ushort)paintRow.RowId));
|
||||
else
|
||||
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
|
||||
}
|
||||
|
||||
return paintList.OrderBy(p => p.Value.Value).ToArray();
|
||||
}
|
||||
|
||||
// Specific icons for tails or ears.
|
||||
private CustomizeData[] GetTailEarShapes(CharaMakeParams row)
|
||||
=> row.Menus.Cast<CharaMakeParams.Menu?>()
|
||||
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values
|
||||
.Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray()
|
||||
?? Array.Empty<CustomizeData>();
|
||||
|
||||
// Specific icons for faces.
|
||||
private CustomizeData[] GetFaces(CharaMakeParams row)
|
||||
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx)
|
||||
?.Values
|
||||
.Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray()
|
||||
?? Array.Empty<CustomizeData>();
|
||||
|
||||
// Specific icons for Hrothgar patterns.
|
||||
private CustomizeData[] HrothgarFurPattern(CharaMakeParams row)
|
||||
=> row.Menus.Cast<CharaMakeParams.Menu?>()
|
||||
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values
|
||||
.Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray()
|
||||
?? Array.Empty<CustomizeData>();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
using System;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
public unsafe struct Customize
|
||||
{
|
||||
public Penumbra.GameData.Structs.CustomizeData Data;
|
||||
|
||||
public Customize(in Penumbra.GameData.Structs.CustomizeData data)
|
||||
{
|
||||
Data = data.Clone();
|
||||
}
|
||||
|
||||
public Race Race
|
||||
{
|
||||
get => (Race)Data.Get(CustomizeIndex.Race).Value;
|
||||
set => Data.Set(CustomizeIndex.Race, (CustomizeValue)(byte)value);
|
||||
}
|
||||
|
||||
public Gender Gender
|
||||
{
|
||||
get => (Gender)Data.Get(CustomizeIndex.Gender).Value + 1;
|
||||
set => Data.Set(CustomizeIndex.Gender, (CustomizeValue)(byte)value - 1);
|
||||
}
|
||||
|
||||
public CustomizeValue BodyType
|
||||
{
|
||||
get => Data.Get(CustomizeIndex.BodyType);
|
||||
set => Data.Set(CustomizeIndex.BodyType, value);
|
||||
}
|
||||
|
||||
public SubRace Clan
|
||||
{
|
||||
get => (SubRace)Data.Get(CustomizeIndex.Clan).Value;
|
||||
set => Data.Set(CustomizeIndex.Clan, (CustomizeValue)(byte)value);
|
||||
}
|
||||
|
||||
public CustomizeValue Face
|
||||
{
|
||||
get => Data.Get(CustomizeIndex.Face);
|
||||
set => Data.Set(CustomizeIndex.Face, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly Customize Default = GenerateDefault();
|
||||
public static readonly Customize Empty = new();
|
||||
|
||||
public CustomizeValue Get(CustomizeIndex index)
|
||||
=> Data.Get(index);
|
||||
|
||||
public bool Set(CustomizeIndex flag, CustomizeValue index)
|
||||
=> Data.Set(flag, index);
|
||||
|
||||
public bool Equals(Customize other)
|
||||
=> Equals(Data, other.Data);
|
||||
|
||||
public CustomizeValue this[CustomizeIndex index]
|
||||
{
|
||||
get => Get(index);
|
||||
set => Set(index, value);
|
||||
}
|
||||
|
||||
private static Customize GenerateDefault()
|
||||
{
|
||||
var ret = new Customize
|
||||
{
|
||||
Race = Race.Hyur,
|
||||
Clan = SubRace.Midlander,
|
||||
Gender = Gender.Male,
|
||||
};
|
||||
ret.Set(CustomizeIndex.BodyType, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.Height, (CustomizeValue)50);
|
||||
ret.Set(CustomizeIndex.Face, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.Hairstyle, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.SkinColor, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.EyeColorRight, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.HighlightsColor, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.TattooColor, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.Eyebrows, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.EyeColorLeft, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.EyeShape, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.Nose, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.Jaw, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.Mouth, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.LipColor, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.MuscleMass, (CustomizeValue)50);
|
||||
ret.Set(CustomizeIndex.TailShape, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.BustSize, (CustomizeValue)50);
|
||||
ret.Set(CustomizeIndex.FacePaint, (CustomizeValue)1);
|
||||
ret.Set(CustomizeIndex.FacePaintColor, (CustomizeValue)1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void Load(Customize other)
|
||||
=> Data.Read(&other.Data);
|
||||
|
||||
public readonly void Write(nint target)
|
||||
=> Data.Write((void*)target);
|
||||
|
||||
public bool LoadBase64(string data)
|
||||
=> Data.LoadBase64(data);
|
||||
|
||||
public readonly string WriteBase64()
|
||||
=> Data.WriteBase64();
|
||||
|
||||
public static CustomizeFlag Compare(Customize lhs, Customize rhs)
|
||||
{
|
||||
CustomizeFlag ret = 0;
|
||||
foreach (var idx in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
var l = lhs[idx];
|
||||
var r = rhs[idx];
|
||||
if (l.Value != r.Value)
|
||||
ret |= idx.ToFlag();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> Data.ToString();
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
[Flags]
|
||||
public enum CustomizeFlag : ulong
|
||||
{
|
||||
Invalid = 0,
|
||||
Race = 1ul << CustomizeIndex.Race,
|
||||
Gender = 1ul << CustomizeIndex.Gender,
|
||||
BodyType = 1ul << CustomizeIndex.BodyType,
|
||||
Height = 1ul << CustomizeIndex.Height,
|
||||
Clan = 1ul << CustomizeIndex.Clan,
|
||||
Face = 1ul << CustomizeIndex.Face,
|
||||
Hairstyle = 1ul << CustomizeIndex.Hairstyle,
|
||||
Highlights = 1ul << CustomizeIndex.Highlights,
|
||||
SkinColor = 1ul << CustomizeIndex.SkinColor,
|
||||
EyeColorRight = 1ul << CustomizeIndex.EyeColorRight,
|
||||
HairColor = 1ul << CustomizeIndex.HairColor,
|
||||
HighlightsColor = 1ul << CustomizeIndex.HighlightsColor,
|
||||
FacialFeature1 = 1ul << CustomizeIndex.FacialFeature1,
|
||||
FacialFeature2 = 1ul << CustomizeIndex.FacialFeature2,
|
||||
FacialFeature3 = 1ul << CustomizeIndex.FacialFeature3,
|
||||
FacialFeature4 = 1ul << CustomizeIndex.FacialFeature4,
|
||||
FacialFeature5 = 1ul << CustomizeIndex.FacialFeature5,
|
||||
FacialFeature6 = 1ul << CustomizeIndex.FacialFeature6,
|
||||
FacialFeature7 = 1ul << CustomizeIndex.FacialFeature7,
|
||||
LegacyTattoo = 1ul << CustomizeIndex.LegacyTattoo,
|
||||
TattooColor = 1ul << CustomizeIndex.TattooColor,
|
||||
Eyebrows = 1ul << CustomizeIndex.Eyebrows,
|
||||
EyeColorLeft = 1ul << CustomizeIndex.EyeColorLeft,
|
||||
EyeShape = 1ul << CustomizeIndex.EyeShape,
|
||||
SmallIris = 1ul << CustomizeIndex.SmallIris,
|
||||
Nose = 1ul << CustomizeIndex.Nose,
|
||||
Jaw = 1ul << CustomizeIndex.Jaw,
|
||||
Mouth = 1ul << CustomizeIndex.Mouth,
|
||||
Lipstick = 1ul << CustomizeIndex.Lipstick,
|
||||
LipColor = 1ul << CustomizeIndex.LipColor,
|
||||
MuscleMass = 1ul << CustomizeIndex.MuscleMass,
|
||||
TailShape = 1ul << CustomizeIndex.TailShape,
|
||||
BustSize = 1ul << CustomizeIndex.BustSize,
|
||||
FacePaint = 1ul << CustomizeIndex.FacePaint,
|
||||
FacePaintReversed = 1ul << CustomizeIndex.FacePaintReversed,
|
||||
FacePaintColor = 1ul << CustomizeIndex.FacePaintColor,
|
||||
}
|
||||
|
||||
public static class CustomizeFlagExtensions
|
||||
{
|
||||
public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul);
|
||||
public const CustomizeFlag AllRelevant = All & ~CustomizeFlag.BodyType & ~CustomizeFlag.Race;
|
||||
|
||||
public const CustomizeFlag RedrawRequired =
|
||||
CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face | CustomizeFlag.BodyType;
|
||||
|
||||
public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizationSet set)
|
||||
=> flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender);
|
||||
|
||||
public static bool RequiresRedraw(this CustomizeFlag flags)
|
||||
=> (flags & RedrawRequired) != 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static CustomizeIndex ToIndex(this CustomizeFlag flag)
|
||||
=> flag switch
|
||||
{
|
||||
CustomizeFlag.Race => CustomizeIndex.Race,
|
||||
CustomizeFlag.Gender => CustomizeIndex.Gender,
|
||||
CustomizeFlag.BodyType => CustomizeIndex.BodyType,
|
||||
CustomizeFlag.Height => CustomizeIndex.Height,
|
||||
CustomizeFlag.Clan => CustomizeIndex.Clan,
|
||||
CustomizeFlag.Face => CustomizeIndex.Face,
|
||||
CustomizeFlag.Hairstyle => CustomizeIndex.Hairstyle,
|
||||
CustomizeFlag.Highlights => CustomizeIndex.Highlights,
|
||||
CustomizeFlag.SkinColor => CustomizeIndex.SkinColor,
|
||||
CustomizeFlag.EyeColorRight => CustomizeIndex.EyeColorRight,
|
||||
CustomizeFlag.HairColor => CustomizeIndex.HairColor,
|
||||
CustomizeFlag.HighlightsColor => CustomizeIndex.HighlightsColor,
|
||||
CustomizeFlag.FacialFeature1 => CustomizeIndex.FacialFeature1,
|
||||
CustomizeFlag.FacialFeature2 => CustomizeIndex.FacialFeature2,
|
||||
CustomizeFlag.FacialFeature3 => CustomizeIndex.FacialFeature3,
|
||||
CustomizeFlag.FacialFeature4 => CustomizeIndex.FacialFeature4,
|
||||
CustomizeFlag.FacialFeature5 => CustomizeIndex.FacialFeature5,
|
||||
CustomizeFlag.FacialFeature6 => CustomizeIndex.FacialFeature6,
|
||||
CustomizeFlag.FacialFeature7 => CustomizeIndex.FacialFeature7,
|
||||
CustomizeFlag.LegacyTattoo => CustomizeIndex.LegacyTattoo,
|
||||
CustomizeFlag.TattooColor => CustomizeIndex.TattooColor,
|
||||
CustomizeFlag.Eyebrows => CustomizeIndex.Eyebrows,
|
||||
CustomizeFlag.EyeColorLeft => CustomizeIndex.EyeColorLeft,
|
||||
CustomizeFlag.EyeShape => CustomizeIndex.EyeShape,
|
||||
CustomizeFlag.SmallIris => CustomizeIndex.SmallIris,
|
||||
CustomizeFlag.Nose => CustomizeIndex.Nose,
|
||||
CustomizeFlag.Jaw => CustomizeIndex.Jaw,
|
||||
CustomizeFlag.Mouth => CustomizeIndex.Mouth,
|
||||
CustomizeFlag.Lipstick => CustomizeIndex.Lipstick,
|
||||
CustomizeFlag.LipColor => CustomizeIndex.LipColor,
|
||||
CustomizeFlag.MuscleMass => CustomizeIndex.MuscleMass,
|
||||
CustomizeFlag.TailShape => CustomizeIndex.TailShape,
|
||||
CustomizeFlag.BustSize => CustomizeIndex.BustSize,
|
||||
CustomizeFlag.FacePaint => CustomizeIndex.FacePaint,
|
||||
CustomizeFlag.FacePaintReversed => CustomizeIndex.FacePaintReversed,
|
||||
CustomizeFlag.FacePaintColor => CustomizeIndex.FacePaintColor,
|
||||
_ => (CustomizeIndex)byte.MaxValue,
|
||||
};
|
||||
|
||||
public static bool SetIfDifferent(ref this CustomizeFlag flags, CustomizeFlag flag, bool value)
|
||||
{
|
||||
var newValue = value ? flags | flag : flags & ~flag;
|
||||
if (newValue == flags)
|
||||
return false;
|
||||
|
||||
flags = newValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
public enum CustomizeIndex : byte
|
||||
{
|
||||
Race,
|
||||
Gender,
|
||||
BodyType,
|
||||
Height,
|
||||
Clan,
|
||||
Face,
|
||||
Hairstyle,
|
||||
Highlights,
|
||||
SkinColor,
|
||||
EyeColorRight,
|
||||
HairColor,
|
||||
HighlightsColor,
|
||||
FacialFeature1,
|
||||
FacialFeature2,
|
||||
FacialFeature3,
|
||||
FacialFeature4,
|
||||
FacialFeature5,
|
||||
FacialFeature6,
|
||||
FacialFeature7,
|
||||
LegacyTattoo,
|
||||
TattooColor,
|
||||
Eyebrows,
|
||||
EyeColorLeft,
|
||||
EyeShape,
|
||||
SmallIris,
|
||||
Nose,
|
||||
Jaw,
|
||||
Mouth,
|
||||
Lipstick,
|
||||
LipColor,
|
||||
MuscleMass,
|
||||
TailShape,
|
||||
BustSize,
|
||||
FacePaint,
|
||||
FacePaintReversed,
|
||||
FacePaintColor,
|
||||
}
|
||||
|
||||
public static class CustomizationExtensions
|
||||
{
|
||||
public const int NumIndices = (int)CustomizeIndex.FacePaintColor + 1;
|
||||
|
||||
public static readonly CustomizeIndex[] All = Enum.GetValues<CustomizeIndex>()
|
||||
.Where(v => v is not CustomizeIndex.Race and not CustomizeIndex.BodyType).ToArray();
|
||||
|
||||
public static readonly CustomizeIndex[] AllBasic = All
|
||||
.Where(v => v is not CustomizeIndex.Gender and not CustomizeIndex.Clan).ToArray();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static (int ByteIdx, byte Mask) ToByteAndMask(this CustomizeIndex index)
|
||||
=> index switch
|
||||
{
|
||||
CustomizeIndex.Race => (0, 0xFF),
|
||||
CustomizeIndex.Gender => (1, 0xFF),
|
||||
CustomizeIndex.BodyType => (2, 0xFF),
|
||||
CustomizeIndex.Height => (3, 0xFF),
|
||||
CustomizeIndex.Clan => (4, 0xFF),
|
||||
CustomizeIndex.Face => (5, 0xFF),
|
||||
CustomizeIndex.Hairstyle => (6, 0xFF),
|
||||
CustomizeIndex.Highlights => (7, 0x80),
|
||||
CustomizeIndex.SkinColor => (8, 0xFF),
|
||||
CustomizeIndex.EyeColorRight => (9, 0xFF),
|
||||
CustomizeIndex.HairColor => (10, 0xFF),
|
||||
CustomizeIndex.HighlightsColor => (11, 0xFF),
|
||||
CustomizeIndex.FacialFeature1 => (12, 0x01),
|
||||
CustomizeIndex.FacialFeature2 => (12, 0x02),
|
||||
CustomizeIndex.FacialFeature3 => (12, 0x04),
|
||||
CustomizeIndex.FacialFeature4 => (12, 0x08),
|
||||
CustomizeIndex.FacialFeature5 => (12, 0x10),
|
||||
CustomizeIndex.FacialFeature6 => (12, 0x20),
|
||||
CustomizeIndex.FacialFeature7 => (12, 0x40),
|
||||
CustomizeIndex.LegacyTattoo => (12, 0x80),
|
||||
CustomizeIndex.TattooColor => (13, 0xFF),
|
||||
CustomizeIndex.Eyebrows => (14, 0xFF),
|
||||
CustomizeIndex.EyeColorLeft => (15, 0xFF),
|
||||
CustomizeIndex.EyeShape => (16, 0x7F),
|
||||
CustomizeIndex.SmallIris => (16, 0x80),
|
||||
CustomizeIndex.Nose => (17, 0xFF),
|
||||
CustomizeIndex.Jaw => (18, 0xFF),
|
||||
CustomizeIndex.Mouth => (19, 0x7F),
|
||||
CustomizeIndex.Lipstick => (19, 0x80),
|
||||
CustomizeIndex.LipColor => (20, 0xFF),
|
||||
CustomizeIndex.MuscleMass => (21, 0xFF),
|
||||
CustomizeIndex.TailShape => (22, 0xFF),
|
||||
CustomizeIndex.BustSize => (23, 0xFF),
|
||||
CustomizeIndex.FacePaint => (24, 0x7F),
|
||||
CustomizeIndex.FacePaintReversed => (24, 0x80),
|
||||
CustomizeIndex.FacePaintColor => (25, 0xFF),
|
||||
_ => (0, 0x00),
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static CustomizeFlag ToFlag(this CustomizeIndex index)
|
||||
=> (CustomizeFlag)(1ul << (int)index);
|
||||
|
||||
public static string ToDefaultName(this CustomizeIndex customizeIndex)
|
||||
=> customizeIndex switch
|
||||
{
|
||||
CustomizeIndex.Race => "Race",
|
||||
CustomizeIndex.Gender => "Gender",
|
||||
CustomizeIndex.BodyType => "Body Type",
|
||||
CustomizeIndex.Height => "Height",
|
||||
CustomizeIndex.Clan => "Clan",
|
||||
CustomizeIndex.Face => "Head Style",
|
||||
CustomizeIndex.Hairstyle => "Hair Style",
|
||||
CustomizeIndex.Highlights => "Highlights",
|
||||
CustomizeIndex.SkinColor => "Skin Color",
|
||||
CustomizeIndex.EyeColorRight => "Right Eye Color",
|
||||
CustomizeIndex.HairColor => "Hair Color",
|
||||
CustomizeIndex.HighlightsColor => "Highlights Color",
|
||||
CustomizeIndex.TattooColor => "Tattoo Color",
|
||||
CustomizeIndex.Eyebrows => "Eyebrow Style",
|
||||
CustomizeIndex.EyeColorLeft => "Left Eye Color",
|
||||
CustomizeIndex.EyeShape => "Small Pupils",
|
||||
CustomizeIndex.Nose => "Nose Style",
|
||||
CustomizeIndex.Jaw => "Jaw Style",
|
||||
CustomizeIndex.Mouth => "Mouth Style",
|
||||
CustomizeIndex.MuscleMass => "Muscle Tone",
|
||||
CustomizeIndex.TailShape => "Tail Shape",
|
||||
CustomizeIndex.BustSize => "Bust Size",
|
||||
CustomizeIndex.FacePaint => "Face Paint",
|
||||
CustomizeIndex.FacePaintColor => "Face Paint Color",
|
||||
CustomizeIndex.LipColor => "Lip Color",
|
||||
CustomizeIndex.FacialFeature1 => "Facial Feature 1",
|
||||
CustomizeIndex.FacialFeature2 => "Facial Feature 2",
|
||||
CustomizeIndex.FacialFeature3 => "Facial Feature 3",
|
||||
CustomizeIndex.FacialFeature4 => "Facial Feature 4",
|
||||
CustomizeIndex.FacialFeature5 => "Facial Feature 5",
|
||||
CustomizeIndex.FacialFeature6 => "Facial Feature 6",
|
||||
CustomizeIndex.FacialFeature7 => "Facial Feature 7",
|
||||
CustomizeIndex.LegacyTattoo => "Legacy Tattoo",
|
||||
CustomizeIndex.SmallIris => "Small Iris",
|
||||
CustomizeIndex.Lipstick => "Enable Lipstick",
|
||||
CustomizeIndex.FacePaintReversed => "Reverse Face Paint",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static unsafe CustomizeValue Get(this in Penumbra.GameData.Structs.CustomizeData data, CustomizeIndex index)
|
||||
{
|
||||
var (offset, mask) = index.ToByteAndMask();
|
||||
return (CustomizeValue)(data.Data[offset] & mask);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static unsafe bool Set(this ref Penumbra.GameData.Structs.CustomizeData data, CustomizeIndex index, CustomizeValue value)
|
||||
{
|
||||
var (offset, mask) = index.ToByteAndMask();
|
||||
return mask != 0xFF
|
||||
? SetIfDifferentMasked(ref data.Data[offset], value, mask)
|
||||
: SetIfDifferent(ref data.Data[offset], value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool SetIfDifferentMasked(ref byte oldValue, CustomizeValue newValue, byte mask)
|
||||
{
|
||||
var tmp = (byte)(newValue.Value & mask);
|
||||
tmp = (byte)(tmp | (oldValue & ~mask));
|
||||
if (oldValue == tmp)
|
||||
return false;
|
||||
|
||||
oldValue = tmp;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool SetIfDifferent(ref byte oldValue, CustomizeValue newValue)
|
||||
{
|
||||
if (oldValue == newValue.Value)
|
||||
return false;
|
||||
|
||||
oldValue = newValue.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
namespace Glamourer.Customization;
|
||||
|
||||
public record struct CustomizeValue(byte Value)
|
||||
{
|
||||
public static readonly CustomizeValue Zero = new(0);
|
||||
public static readonly CustomizeValue Max = new(0xFF);
|
||||
|
||||
public static CustomizeValue Bool(bool b)
|
||||
=> b ? Max : Zero;
|
||||
|
||||
public static explicit operator CustomizeValue(byte value)
|
||||
=> new(value);
|
||||
|
||||
public static CustomizeValue operator ++(CustomizeValue v)
|
||||
=> new(++v.Value);
|
||||
|
||||
public static CustomizeValue operator --(CustomizeValue v)
|
||||
=> new(--v.Value);
|
||||
|
||||
public static bool operator <(CustomizeValue v, int count)
|
||||
=> v.Value < count;
|
||||
|
||||
public static bool operator >(CustomizeValue v, int count)
|
||||
=> v.Value > count;
|
||||
|
||||
public static CustomizeValue operator +(CustomizeValue v, int rhs)
|
||||
=> new((byte)(v.Value + rhs));
|
||||
|
||||
public static CustomizeValue operator -(CustomizeValue v, int rhs)
|
||||
=> new((byte)(v.Value - rhs));
|
||||
|
||||
public override string ToString()
|
||||
=> Value.ToString();
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Dalamud.Memory;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = Size)]
|
||||
public unsafe struct DatCharacterFile
|
||||
{
|
||||
public const int Size = 4 + 4 + 4 + 4 + Penumbra.GameData.Structs.CustomizeData.Size + 2 + 4 + 41 * 4; // 212
|
||||
|
||||
[FieldOffset(0)]
|
||||
private fixed byte _data[Size];
|
||||
|
||||
[FieldOffset(0)]
|
||||
public readonly uint Magic = 0x2013FF14;
|
||||
|
||||
[FieldOffset(4)]
|
||||
public readonly uint Version = 0x05;
|
||||
|
||||
[FieldOffset(8)]
|
||||
private uint _checksum;
|
||||
|
||||
[FieldOffset(12)]
|
||||
private readonly uint _padding = 0;
|
||||
|
||||
[FieldOffset(16)]
|
||||
private Penumbra.GameData.Structs.CustomizeData _customize;
|
||||
|
||||
[FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size)]
|
||||
private ushort _voice;
|
||||
|
||||
[FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size + 2)]
|
||||
private uint _timeStamp;
|
||||
|
||||
[FieldOffset(Size - 41 * 4)]
|
||||
private fixed byte _description[41 * 4];
|
||||
|
||||
public readonly void Write(Stream stream)
|
||||
{
|
||||
for (var i = 0; i < Size; ++i)
|
||||
stream.WriteByte(_data[i]);
|
||||
}
|
||||
|
||||
public static bool Read(Stream stream, out DatCharacterFile file)
|
||||
{
|
||||
if (stream.Length - stream.Position != Size)
|
||||
{
|
||||
file = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
file = new DatCharacterFile(stream);
|
||||
return true;
|
||||
}
|
||||
|
||||
private DatCharacterFile(Stream stream)
|
||||
{
|
||||
for (var i = 0; i < Size; ++i)
|
||||
_data[i] = (byte)stream.ReadByte();
|
||||
}
|
||||
|
||||
public DatCharacterFile(in Customize customize, byte voice, string text)
|
||||
{
|
||||
SetCustomize(customize);
|
||||
SetVoice(voice);
|
||||
SetTime(DateTimeOffset.UtcNow);
|
||||
SetDescription(text);
|
||||
_checksum = CalculateChecksum();
|
||||
}
|
||||
|
||||
public readonly uint CalculateChecksum()
|
||||
{
|
||||
var ret = 0u;
|
||||
for (var i = 16; i < Size; i++)
|
||||
ret ^= (uint)(_data[i] << ((i - 16) % 24));
|
||||
return ret;
|
||||
}
|
||||
|
||||
public readonly uint Checksum
|
||||
=> _checksum;
|
||||
|
||||
public Customize Customize
|
||||
{
|
||||
readonly get => new(_customize);
|
||||
set
|
||||
{
|
||||
SetCustomize(value);
|
||||
_checksum = CalculateChecksum();
|
||||
}
|
||||
}
|
||||
|
||||
public ushort Voice
|
||||
{
|
||||
readonly get => _voice;
|
||||
set
|
||||
{
|
||||
SetVoice(value);
|
||||
_checksum = CalculateChecksum();
|
||||
}
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
readonly get
|
||||
{
|
||||
fixed (byte* ptr = _description)
|
||||
{
|
||||
return MemoryHelper.ReadStringNullTerminated((nint)ptr);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
SetDescription(value);
|
||||
_checksum = CalculateChecksum();
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset Time
|
||||
{
|
||||
readonly get => DateTimeOffset.FromUnixTimeSeconds(_timeStamp);
|
||||
set
|
||||
{
|
||||
SetTime(value);
|
||||
_checksum = CalculateChecksum();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTime(DateTimeOffset time)
|
||||
=> _timeStamp = (uint)time.ToUnixTimeSeconds();
|
||||
|
||||
private void SetCustomize(in Customize customize)
|
||||
=> _customize = customize.Data.Clone();
|
||||
|
||||
private void SetVoice(ushort voice)
|
||||
=> _voice = voice;
|
||||
|
||||
private void SetDescription(string text)
|
||||
{
|
||||
fixed (byte* ptr = _description)
|
||||
{
|
||||
var span = new Span<byte>(ptr, 41 * 4);
|
||||
Encoding.UTF8.GetBytes(text.AsSpan(0, Math.Min(40, text.Length)), span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
||||
public interface ICustomizationManager
|
||||
{
|
||||
public IReadOnlyList<Race> Races { get; }
|
||||
public IReadOnlyList<SubRace> Clans { get; }
|
||||
public IReadOnlyList<Gender> Genders { get; }
|
||||
|
||||
public CustomizationSet GetList(SubRace race, Gender gender);
|
||||
|
||||
public IDalamudTextureWrap GetIcon(uint iconId);
|
||||
public string GetName(CustomName name);
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Glamourer.Structs;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Glamourer;
|
||||
|
||||
public static class GameData
|
||||
{
|
||||
private static Dictionary<byte, Job>? _jobs;
|
||||
private static Dictionary<ushort, JobGroup>? _jobGroups;
|
||||
private static JobGroup[]? _allJobGroups;
|
||||
|
||||
public static IReadOnlyDictionary<byte, Job> Jobs(IDataManager dataManager)
|
||||
{
|
||||
if (_jobs != null)
|
||||
return _jobs;
|
||||
|
||||
var sheet = dataManager.GetExcelSheet<ClassJob>()!;
|
||||
_jobs = sheet.Where(j => j.Abbreviation.RawData.Length > 0).ToDictionary(j => (byte)j.RowId, j => new Job(j));
|
||||
return _jobs;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<JobGroup> AllJobGroups(IDataManager dataManager)
|
||||
{
|
||||
if (_allJobGroups != null)
|
||||
return _allJobGroups;
|
||||
|
||||
var sheet = dataManager.GetExcelSheet<ClassJobCategory>()!;
|
||||
var jobs = dataManager.GetExcelSheet<ClassJob>(ClientLanguage.English)!;
|
||||
_allJobGroups = sheet.Select(j => new JobGroup(j, jobs)).ToArray();
|
||||
return _allJobGroups;
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<ushort, JobGroup> JobGroups(IDataManager dataManager)
|
||||
{
|
||||
if (_jobGroups != null)
|
||||
return _jobGroups;
|
||||
|
||||
static bool ValidIndex(uint idx)
|
||||
{
|
||||
if (idx is > 0 and < 36)
|
||||
return true;
|
||||
|
||||
return idx switch
|
||||
{
|
||||
// Single jobs and big groups
|
||||
91 => true,
|
||||
92 => true,
|
||||
96 => true,
|
||||
98 => true,
|
||||
99 => true,
|
||||
111 => true,
|
||||
112 => true,
|
||||
129 => true,
|
||||
149 => true,
|
||||
150 => true,
|
||||
156 => true,
|
||||
157 => true,
|
||||
158 => true,
|
||||
159 => true,
|
||||
180 => true,
|
||||
181 => true,
|
||||
188 => true,
|
||||
189 => true,
|
||||
|
||||
// Class + Job
|
||||
38 => true,
|
||||
41 => true,
|
||||
44 => true,
|
||||
47 => true,
|
||||
50 => true,
|
||||
53 => true,
|
||||
55 => true,
|
||||
69 => true,
|
||||
68 => true,
|
||||
93 => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
_jobGroups = AllJobGroups(dataManager).Where(j => ValidIndex(j.Id))
|
||||
.ToDictionary(j => (ushort) j.Id, j => j);
|
||||
return _jobGroups;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>Glamourer</RootNamespace>
|
||||
<AssemblyName>Glamourer.GameData</AssemblyName>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<Company>SoftOtter</Company>
|
||||
<Product>Glamourer</Product>
|
||||
<Copyright>Copyright © 2020</Copyright>
|
||||
<Deterministic>true</Deterministic>
|
||||
<OutputType>Library</OutputType>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
|
||||
</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>$(DALAMUD_ROOT)\Dalamud.dll</HintPath>
|
||||
<HintPath>..\libs\Dalamud.dll</HintPath>
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(DALAMUD_ROOT)\Lumina.dll</HintPath>
|
||||
<HintPath>..\libs\Lumina.dll</HintPath>
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(DALAMUD_ROOT)\Lumina.Excel.dll</HintPath>
|
||||
<HintPath>..\libs\Lumina.Excel.dll</HintPath>
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Penumbra.GameData">
|
||||
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Util\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<RootNamespace>Glamourer</RootNamespace>
|
||||
<AssemblyName>Glamourer.GameData</AssemblyName>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<Company>SoftOtter</Company>
|
||||
<Product>Glamourer</Product>
|
||||
<Copyright>Copyright © 2020</Copyright>
|
||||
<Deterministic>true</Deterministic>
|
||||
<OutputType>Library</OutputType>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</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>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGuiScene">
|
||||
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
<ProjectReference Include="..\OtterGui\OtterGui.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
namespace Glamourer;
|
||||
|
||||
public static class Offsets
|
||||
{
|
||||
public static class Character
|
||||
{
|
||||
public const int ClassJobContainer = 0x1A8;
|
||||
}
|
||||
|
||||
public const byte DrawObjectVisorStateFlag = 0x40;
|
||||
public const byte DrawObjectVisorToggleFlag = 0x80;
|
||||
}
|
||||
|
||||
public static class Sigs
|
||||
{
|
||||
public const string ChangeJob = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 80 61";
|
||||
public const string FlagSlotForUpdate = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A";
|
||||
public const string ChangeCustomize = "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86";
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
using System;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Structs;
|
||||
|
||||
[Flags]
|
||||
public enum EquipFlag : uint
|
||||
{
|
||||
Head = 0x00000001,
|
||||
Body = 0x00000002,
|
||||
Hands = 0x00000004,
|
||||
Legs = 0x00000008,
|
||||
Feet = 0x00000010,
|
||||
Ears = 0x00000020,
|
||||
Neck = 0x00000040,
|
||||
Wrist = 0x00000080,
|
||||
RFinger = 0x00000100,
|
||||
LFinger = 0x00000200,
|
||||
Mainhand = 0x00000400,
|
||||
Offhand = 0x00000800,
|
||||
HeadStain = 0x00001000,
|
||||
BodyStain = 0x00002000,
|
||||
HandsStain = 0x00004000,
|
||||
LegsStain = 0x00008000,
|
||||
FeetStain = 0x00010000,
|
||||
EarsStain = 0x00020000,
|
||||
NeckStain = 0x00040000,
|
||||
WristStain = 0x00080000,
|
||||
RFingerStain = 0x00100000,
|
||||
LFingerStain = 0x00200000,
|
||||
MainhandStain = 0x00400000,
|
||||
OffhandStain = 0x00800000,
|
||||
}
|
||||
|
||||
public static class EquipFlagExtensions
|
||||
{
|
||||
public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1);
|
||||
public const int NumEquipFlags = 24;
|
||||
|
||||
public static EquipFlag ToFlag(this EquipSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.MainHand => EquipFlag.Mainhand,
|
||||
EquipSlot.OffHand => EquipFlag.Offhand,
|
||||
EquipSlot.Head => EquipFlag.Head,
|
||||
EquipSlot.Body => EquipFlag.Body,
|
||||
EquipSlot.Hands => EquipFlag.Hands,
|
||||
EquipSlot.Legs => EquipFlag.Legs,
|
||||
EquipSlot.Feet => EquipFlag.Feet,
|
||||
EquipSlot.Ears => EquipFlag.Ears,
|
||||
EquipSlot.Neck => EquipFlag.Neck,
|
||||
EquipSlot.Wrists => EquipFlag.Wrist,
|
||||
EquipSlot.RFinger => EquipFlag.RFinger,
|
||||
EquipSlot.LFinger => EquipFlag.LFinger,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
public static EquipFlag ToStainFlag(this EquipSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.MainHand => EquipFlag.MainhandStain,
|
||||
EquipSlot.OffHand => EquipFlag.OffhandStain,
|
||||
EquipSlot.Head => EquipFlag.HeadStain,
|
||||
EquipSlot.Body => EquipFlag.BodyStain,
|
||||
EquipSlot.Hands => EquipFlag.HandsStain,
|
||||
EquipSlot.Legs => EquipFlag.LegsStain,
|
||||
EquipSlot.Feet => EquipFlag.FeetStain,
|
||||
EquipSlot.Ears => EquipFlag.EarsStain,
|
||||
EquipSlot.Neck => EquipFlag.NeckStain,
|
||||
EquipSlot.Wrists => EquipFlag.WristStain,
|
||||
EquipSlot.RFinger => EquipFlag.RFingerStain,
|
||||
EquipSlot.LFinger => EquipFlag.LFingerStain,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
public static EquipFlag ToBothFlags(this EquipSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain,
|
||||
EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain,
|
||||
EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain,
|
||||
EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain,
|
||||
EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain,
|
||||
EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain,
|
||||
EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain,
|
||||
EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain,
|
||||
EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain,
|
||||
EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain,
|
||||
EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain,
|
||||
EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
using Dalamud.Utility;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Glamourer.Structs;
|
||||
|
||||
// A struct containing the different jobs the game supports.
|
||||
// Also contains the jobs Name and Abbreviation as strings.
|
||||
public readonly struct Job
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string Abbreviation;
|
||||
public readonly ClassJob Base;
|
||||
|
||||
public uint Id
|
||||
=> Base.RowId;
|
||||
|
||||
public JobFlag Flag
|
||||
=> (JobFlag)(1ul << (int)Base.RowId);
|
||||
|
||||
public Job(ClassJob job)
|
||||
{
|
||||
Base = job;
|
||||
Name = job.Name.ToDalamudString().ToString();
|
||||
Abbreviation = job.Abbreviation.ToDalamudString().ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> Name;
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Glamourer.Structs;
|
||||
|
||||
[Flags]
|
||||
public enum JobFlag : ulong
|
||||
{ }
|
||||
|
||||
// The game specifies different job groups that can contain specific jobs or not.
|
||||
public readonly struct JobGroup
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly int Count;
|
||||
public readonly uint Id;
|
||||
private readonly JobFlag _flags;
|
||||
|
||||
// Create a job group from a given category and the ClassJob sheet.
|
||||
// It looks up the different jobs contained in the category and sets the flags appropriately.
|
||||
public JobGroup(ClassJobCategory group, ExcelSheet<ClassJob> jobs)
|
||||
{
|
||||
Count = 0;
|
||||
_flags = 0ul;
|
||||
Id = group.RowId;
|
||||
Name = group.Name.ToString();
|
||||
|
||||
Debug.Assert(jobs.RowCount < 64, $"Number of Jobs exceeded 63 ({jobs.RowCount}).");
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
var abbr = job.Abbreviation.ToString();
|
||||
if (abbr.Length == 0)
|
||||
continue;
|
||||
|
||||
var prop = group.GetType().GetProperty(abbr);
|
||||
Debug.Assert(prop != null, $"Could not get job abbreviation {abbr} property.");
|
||||
|
||||
if (!(bool)prop.GetValue(group)!)
|
||||
continue;
|
||||
|
||||
++Count;
|
||||
_flags |= (JobFlag)(1ul << (int)job.RowId);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a job is contained inside this group.
|
||||
public bool Fits(Job job)
|
||||
=> _flags.HasFlag(job.Flag);
|
||||
|
||||
// Check if any of the jobs in the given flags fit this group.
|
||||
public bool Fits(JobFlag flag)
|
||||
=> (_flags & flag) != 0;
|
||||
|
||||
// Check if a job is contained inside this group.
|
||||
public bool Fits(uint jobId)
|
||||
{
|
||||
var flag = (JobFlag)(1ul << (int)jobId);
|
||||
return _flags.HasFlag(flag);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,12 @@ MinimumVisualStudioVersion = 10.0.40219.1
|
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.github\workflows\release.yml = .github\workflows\release.yml
|
||||
Glamourer\Glamourer.json = Glamourer\Glamourer.json
|
||||
repo.json = repo.json
|
||||
.github\workflows\test_release.yml = .github\workflows\test_release.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.GameData", "Glamourer.GameData\Glamourer.GameData.csproj", "{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}"
|
||||
|
|
@ -21,36 +22,38 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{EF233CE2-F243-449E-BE05-72B9D110E419}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.Api", "Glamourer.Api\Glamourer.Api.csproj", "{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|x64
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|x64
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|x64
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|x64
|
||||
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
133
Glamourer/Api/ApiHelpers.cs
Normal file
133
Glamourer/Api/ApiHelpers.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.State;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public class ApiHelpers(ActorObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal IEnumerable<ActorState> FindExistingStates(string actorName, ushort worldId = ushort.MaxValue)
|
||||
{
|
||||
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
|
||||
yield break;
|
||||
|
||||
if (worldId == WorldId.AnyWorld.Id)
|
||||
{
|
||||
foreach (var state in stateManager.Values.Where(state
|
||||
=> state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString))
|
||||
yield return state;
|
||||
}
|
||||
else
|
||||
{
|
||||
var identifier = actors.CreatePlayer(byteString, worldId);
|
||||
if (stateManager.TryGetValue(identifier, out var state))
|
||||
yield return state;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal GlamourerApiEc FindExistingState(int objectIndex, out ActorState? state)
|
||||
{
|
||||
var actor = objects.Objects[objectIndex];
|
||||
var identifier = actor.GetIdentifier(actors);
|
||||
if (!identifier.IsValid)
|
||||
{
|
||||
state = null;
|
||||
return GlamourerApiEc.ActorNotFound;
|
||||
}
|
||||
|
||||
stateManager.TryGetValue(identifier, out state);
|
||||
return GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal ActorState? FindState(int objectIndex)
|
||||
{
|
||||
var actor = objects.Objects[objectIndex];
|
||||
var identifier = actor.GetIdentifier(actors);
|
||||
if (identifier.IsValid && stateManager.GetOrCreate(identifier, actor, out var state))
|
||||
return state;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal static DesignBase.FlagRestrictionResetter Restrict(DesignBase design, ApplyFlag flags)
|
||||
=> (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) switch
|
||||
{
|
||||
ApplyFlag.Equipment => design.TemporarilyRestrictApplication(ApplicationCollection.Equipment),
|
||||
ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.Customizations),
|
||||
ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.All),
|
||||
_ => design.TemporarilyRestrictApplication(ApplicationCollection.None),
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal static void Lock(ActorState state, uint key, ApplyFlag flags)
|
||||
{
|
||||
if ((flags & ApplyFlag.Lock) != 0 && key != 0)
|
||||
state.Lock(key);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal IEnumerable<ActorState> FindStates(string objectName)
|
||||
{
|
||||
if (objectName.Length == 0 || !ByteString.FromString(objectName, out var byteString))
|
||||
return [];
|
||||
|
||||
return stateManager.Values.Where(state => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString)
|
||||
.Concat(objects
|
||||
.Where(kvp => kvp.Key is { IsValid: true, Type: IdentifierType.Player } && kvp.Key.PlayerName == byteString)
|
||||
.SelectWhere(kvp =>
|
||||
{
|
||||
if (stateManager.ContainsKey(kvp.Key))
|
||||
return (false, null);
|
||||
|
||||
var ret = stateManager.GetOrCreate(kvp.Key, kvp.Value.Objects[0], out var state);
|
||||
return (ret, state);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
internal static GlamourerApiEc Return(GlamourerApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown")
|
||||
{
|
||||
if (ec is GlamourerApiEc.Success or GlamourerApiEc.NothingDone)
|
||||
Glamourer.Log.Verbose($"[{name}] Called with {args}, returned {ec}.");
|
||||
else
|
||||
Glamourer.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(" = ");
|
||||
if (arguments[2 * i + 1] is IEnumerable e)
|
||||
sb.Append($"[{string.Join(',', e)}]");
|
||||
else
|
||||
sb.Append(arguments[2 * i + 1]);
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
return sb.ToString(0, sb.Length - 2);
|
||||
});
|
||||
}
|
||||
}
|
||||
138
Glamourer/Api/DesignsApi.cs
Normal file
138
Glamourer/Api/DesignsApi.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using Glamourer.Api.Api;
|
||||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.State;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Services;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public class DesignsApi(
|
||||
ApiHelpers helpers,
|
||||
DesignManager designs,
|
||||
StateManager stateManager,
|
||||
DesignFileSystem fileSystem,
|
||||
DesignColors color,
|
||||
DesignConverter converter)
|
||||
: IGlamourerApiDesigns, IApiService
|
||||
{
|
||||
public Dictionary<Guid, string> GetDesignList()
|
||||
=> designs.Designs.ToDictionary(d => d.Identifier, d => d.Name.Text);
|
||||
|
||||
public Dictionary<Guid, (string DisplayName, string FullPath, uint DisplayColor, bool ShownInQdb)> GetDesignListExtended()
|
||||
=> fileSystem.ToDictionary(kvp => kvp.Key.Identifier,
|
||||
kvp => (kvp.Key.Name.Text, kvp.Value.FullName(), color.GetColor(kvp.Key), kvp.Key.QuickDesign));
|
||||
|
||||
public (string DisplayName, string FullPath, uint DisplayColor, bool ShowInQdb) GetExtendedDesignData(Guid designId)
|
||||
=> designs.Designs.ByIdentifier(designId) is { } d
|
||||
? (d.Name.Text, fileSystem.TryGetValue(d, out var leaf) ? leaf.FullName() : d.Name.Text, color.GetColor(d), d.QuickDesign)
|
||||
: (string.Empty, string.Empty, 0, false);
|
||||
|
||||
public GlamourerApiEc ApplyDesign(Guid designId, int objectIndex, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Design", designId, "Index", objectIndex, "Key", key, "Flags", flags);
|
||||
var design = designs.Designs.ByIdentifier(designId);
|
||||
if (design == null)
|
||||
return ApiHelpers.Return(GlamourerApiEc.DesignNotFound, args);
|
||||
|
||||
if (helpers.FindState(objectIndex) is not { } state)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
ApplyDesign(state, design, key, flags);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
private void ApplyDesign(ActorState state, Design design, uint key, ApplyFlag flags)
|
||||
{
|
||||
var once = (flags & ApplyFlag.Once) != 0;
|
||||
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true,
|
||||
ResetMaterials: !once && key != 0, IsFinal: true);
|
||||
|
||||
using var restrict = ApiHelpers.Restrict(design, flags);
|
||||
stateManager.ApplyDesign(state, design, settings);
|
||||
}
|
||||
|
||||
public GlamourerApiEc ApplyDesignName(Guid designId, string playerName, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Design", designId, "Name", playerName, "Key", key, "Flags", flags);
|
||||
var design = designs.Designs.ByIdentifier(designId);
|
||||
if (design == null)
|
||||
return ApiHelpers.Return(GlamourerApiEc.DesignNotFound, args);
|
||||
|
||||
var any = false;
|
||||
var anyUnlocked = false;
|
||||
foreach (var state in helpers.FindStates(playerName))
|
||||
{
|
||||
any = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyUnlocked = true;
|
||||
ApplyDesign(state, design, key, flags);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
if (!any)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public (GlamourerApiEc, Guid) AddDesign(string designInput, string name)
|
||||
{
|
||||
var args = ApiHelpers.Args("DesignData", designInput, "Name", name);
|
||||
|
||||
if (converter.FromBase64(designInput, true, true, out _) is not { } designBase)
|
||||
try
|
||||
{
|
||||
var jObj = JObject.Parse(designInput);
|
||||
designBase = converter.FromJObject(jObj, true, true);
|
||||
if (designBase is null)
|
||||
return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Log.Error($"Failure parsing data for AddDesign due to\n{ex}");
|
||||
return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var design = designBase is Design d
|
||||
? designs.CreateClone(d, name, true)
|
||||
: designs.CreateClone(designBase, name, true);
|
||||
return (ApiHelpers.Return(GlamourerApiEc.Success, args), design.Identifier);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Log.Error($"Unknown error creating design via IPC:\n{ex}");
|
||||
return (ApiHelpers.Return(GlamourerApiEc.UnknownError, args), Guid.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public GlamourerApiEc DeleteDesign(Guid designId)
|
||||
{
|
||||
var args = ApiHelpers.Args("DesignId", designId);
|
||||
if (designs.Designs.ByIdentifier(designId) is not { } design)
|
||||
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
designs.Delete(design);
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public string? GetDesignBase64(Guid designId)
|
||||
=> designs.Designs.ByIdentifier(designId) is { } design
|
||||
? converter.ShareBase64(design)
|
||||
: null;
|
||||
|
||||
public JObject? GetDesignJObject(Guid designId)
|
||||
=> designs.Designs.ByIdentifier(designId) is { } design
|
||||
? converter.ShareJObject(design)
|
||||
: null;
|
||||
}
|
||||
25
Glamourer/Api/GlamourerApi.cs
Normal file
25
Glamourer/Api/GlamourerApi.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using Glamourer.Api.Api;
|
||||
using OtterGui.Services;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public class GlamourerApi(Configuration config, DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService
|
||||
{
|
||||
public const int CurrentApiVersionMajor = 1;
|
||||
public const int CurrentApiVersionMinor = 7;
|
||||
|
||||
public (int Major, int Minor) ApiVersion
|
||||
=> (CurrentApiVersionMajor, CurrentApiVersionMinor);
|
||||
|
||||
public bool AutoReloadGearEnabled
|
||||
=> config.AutoRedrawEquipOnChanges;
|
||||
|
||||
public IGlamourerApiDesigns Designs
|
||||
=> designs;
|
||||
|
||||
public IGlamourerApiItems Items
|
||||
=> items;
|
||||
|
||||
public IGlamourerApiState State
|
||||
=> state;
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
using System;
|
||||
using Dalamud.Plugin;
|
||||
using Penumbra.Api.Helpers;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public partial class GlamourerIpc
|
||||
{
|
||||
public const string LabelApiVersion = "Glamourer.ApiVersion";
|
||||
public const string LabelApiVersions = "Glamourer.ApiVersions";
|
||||
|
||||
private readonly FuncProvider<int> _apiVersionProvider;
|
||||
private readonly FuncProvider<(int Major, int Minor)> _apiVersionsProvider;
|
||||
|
||||
public static FuncSubscriber<int> ApiVersionSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApiVersion);
|
||||
|
||||
public static FuncSubscriber<(int Major, int Minor)> ApiVersionsSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApiVersions);
|
||||
|
||||
public int ApiVersion()
|
||||
=> CurrentApiVersionMajor;
|
||||
|
||||
public (int Major, int Minor) ApiVersions()
|
||||
=> (CurrentApiVersionMajor, CurrentApiVersionMinor);
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Interop.Structs;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public partial class GlamourerIpc
|
||||
{
|
||||
public const string LabelApplyAll = "Glamourer.ApplyAll";
|
||||
public const string LabelApplyAllToCharacter = "Glamourer.ApplyAllToCharacter";
|
||||
public const string LabelApplyOnlyEquipment = "Glamourer.ApplyOnlyEquipment";
|
||||
public const string LabelApplyOnlyEquipmentToCharacter = "Glamourer.ApplyOnlyEquipmentToCharacter";
|
||||
public const string LabelApplyOnlyCustomization = "Glamourer.ApplyOnlyCustomization";
|
||||
public const string LabelApplyOnlyCustomizationToCharacter = "Glamourer.ApplyOnlyCustomizationToCharacter";
|
||||
|
||||
public const string LabelApplyAllLock = "Glamourer.ApplyAllLock";
|
||||
public const string LabelApplyAllToCharacterLock = "Glamourer.ApplyAllToCharacterLock";
|
||||
public const string LabelApplyOnlyEquipmentLock = "Glamourer.ApplyOnlyEquipmentLock";
|
||||
public const string LabelApplyOnlyEquipmentToCharacterLock = "Glamourer.ApplyOnlyEquipmentToCharacterLock";
|
||||
public const string LabelApplyOnlyCustomizationLock = "Glamourer.ApplyOnlyCustomizationLock";
|
||||
public const string LabelApplyOnlyCustomizationToCharacterLock = "Glamourer.ApplyOnlyCustomizationToCharacterLock";
|
||||
|
||||
private readonly ActionProvider<string, string> _applyAllProvider;
|
||||
private readonly ActionProvider<string, Character?> _applyAllToCharacterProvider;
|
||||
private readonly ActionProvider<string, string> _applyOnlyEquipmentProvider;
|
||||
private readonly ActionProvider<string, Character?> _applyOnlyEquipmentToCharacterProvider;
|
||||
private readonly ActionProvider<string, string> _applyOnlyCustomizationProvider;
|
||||
private readonly ActionProvider<string, Character?> _applyOnlyCustomizationToCharacterProvider;
|
||||
|
||||
private readonly ActionProvider<string, string, uint> _applyAllProviderLock;
|
||||
private readonly ActionProvider<string, Character?, uint> _applyAllToCharacterProviderLock;
|
||||
private readonly ActionProvider<string, string, uint> _applyOnlyEquipmentProviderLock;
|
||||
private readonly ActionProvider<string, Character?, uint> _applyOnlyEquipmentToCharacterProviderLock;
|
||||
private readonly ActionProvider<string, string, uint> _applyOnlyCustomizationProviderLock;
|
||||
private readonly ActionProvider<string, Character?, uint> _applyOnlyCustomizationToCharacterProviderLock;
|
||||
|
||||
public static ActionSubscriber<string, string> ApplyAllSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApplyAll);
|
||||
|
||||
public static ActionSubscriber<string, Character?> ApplyAllToCharacterSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApplyAllToCharacter);
|
||||
|
||||
public static ActionSubscriber<string, string> ApplyOnlyEquipmentSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApplyOnlyEquipment);
|
||||
|
||||
public static ActionSubscriber<string, Character?> ApplyOnlyEquipmentToCharacterSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApplyOnlyEquipmentToCharacter);
|
||||
|
||||
public static ActionSubscriber<string, string> ApplyOnlyCustomizationSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApplyOnlyCustomization);
|
||||
|
||||
public static ActionSubscriber<string, Character?> ApplyOnlyCustomizationToCharacterSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelApplyOnlyCustomizationToCharacter);
|
||||
|
||||
public void ApplyAll(string base64, string characterName)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0);
|
||||
|
||||
public void ApplyAllToCharacter(string base64, Character? character)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0);
|
||||
|
||||
public void ApplyOnlyEquipment(string base64, string characterName)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, 0);
|
||||
|
||||
public void ApplyOnlyEquipmentToCharacter(string base64, Character? character)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(character), version, 0);
|
||||
|
||||
public void ApplyOnlyCustomization(string base64, string characterName)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(characterName), version, 0);
|
||||
|
||||
public void ApplyOnlyCustomizationToCharacter(string base64, Character? character)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(character), version, 0);
|
||||
|
||||
|
||||
public void ApplyAllLock(string base64, string characterName, uint lockCode)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, lockCode);
|
||||
|
||||
public void ApplyAllToCharacterLock(string base64, Character? character, uint lockCode)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, lockCode);
|
||||
|
||||
public void ApplyOnlyEquipmentLock(string base64, string characterName, uint lockCode)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, lockCode);
|
||||
|
||||
public void ApplyOnlyEquipmentToCharacterLock(string base64, Character? character, uint lockCode)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(character), version, lockCode);
|
||||
|
||||
public void ApplyOnlyCustomizationLock(string base64, string characterName, uint lockCode)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(characterName), version, lockCode);
|
||||
|
||||
public void ApplyOnlyCustomizationToCharacterLock(string base64, Character? character, uint lockCode)
|
||||
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(character), version, lockCode);
|
||||
|
||||
|
||||
private void ApplyDesign(DesignBase? design, IEnumerable<ActorIdentifier> actors, byte version, uint lockCode)
|
||||
{
|
||||
if (design == null)
|
||||
return;
|
||||
|
||||
var hasModelId = version >= 3;
|
||||
_objects.Update();
|
||||
foreach (var id in actors)
|
||||
{
|
||||
if (!_stateManager.TryGetValue(id, out var state))
|
||||
{
|
||||
var data = _objects.TryGetValue(id, out var d) ? d : ActorData.Invalid;
|
||||
if (!data.Valid || !_stateManager.GetOrCreate(id, data.Objects[0], out state))
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((hasModelId || state.ModelData.ModelId == 0) && state.CanUnlock(lockCode))
|
||||
{
|
||||
_stateManager.ApplyDesign(design, state, StateChanged.Source.Ipc, lockCode);
|
||||
state.Lock(lockCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
using System;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Interop.Structs;
|
||||
using Glamourer.State;
|
||||
using Penumbra.Api.Helpers;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public partial class GlamourerIpc
|
||||
{
|
||||
public const string LabelStateChanged = "Glamourer.StateChanged";
|
||||
public const string LabelGPoseChanged = "Glamourer.GPoseChanged";
|
||||
|
||||
private readonly GPoseService _gPose;
|
||||
private readonly StateChanged _stateChangedEvent;
|
||||
private readonly EventProvider<StateChanged.Type, nint, Lazy<string>> _stateChangedProvider;
|
||||
private readonly EventProvider<bool> _gPoseChangedProvider;
|
||||
|
||||
private void OnStateChanged(StateChanged.Type type, StateChanged.Source source, ActorState state, ActorData actors, object? data = null)
|
||||
{
|
||||
foreach (var actor in actors.Objects)
|
||||
_stateChangedProvider.Invoke(type, actor.Address, new Lazy<string>(() => _designConverter.ShareBase64(state)));
|
||||
}
|
||||
|
||||
private void OnGPoseChanged(bool value)
|
||||
=> _gPoseChangedProvider.Invoke(value);
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
using System.Buffers.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Structs;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public partial class GlamourerIpc
|
||||
{
|
||||
public const string LabelGetAllCustomization = "Glamourer.GetAllCustomization";
|
||||
public const string LabelGetAllCustomizationFromCharacter = "Glamourer.GetAllCustomizationFromCharacter";
|
||||
|
||||
private readonly FuncProvider<string, string?> _getAllCustomizationProvider;
|
||||
private readonly FuncProvider<Character?, string?> _getAllCustomizationFromCharacterProvider;
|
||||
|
||||
public static FuncSubscriber<string, string?> GetAllCustomizationSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelGetAllCustomization);
|
||||
|
||||
public static FuncSubscriber<Character?, string?> GetAllCustomizationFromCharacterSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelGetAllCustomizationFromCharacter);
|
||||
|
||||
public string? GetAllCustomization(string characterName)
|
||||
=> GetCustomization(FindActors(characterName));
|
||||
|
||||
public string? GetAllCustomizationFromCharacter(Character? character)
|
||||
=> GetCustomization(FindActors(character));
|
||||
|
||||
private string? GetCustomization(IEnumerable<ActorIdentifier> actors)
|
||||
{
|
||||
var actor = actors.FirstOrDefault(ActorIdentifier.Invalid);
|
||||
if (!actor.IsValid)
|
||||
return null;
|
||||
|
||||
if (!_stateManager.TryGetValue(actor, out var state))
|
||||
{
|
||||
_objects.Update();
|
||||
if (!_objects.TryGetValue(actor, out var data) || !data.Valid)
|
||||
return null;
|
||||
if (!_stateManager.GetOrCreate(actor, data.Objects[0], out state))
|
||||
return null;
|
||||
}
|
||||
|
||||
return _designConverter.ShareBase64(state);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Glamourer.Events;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public partial class GlamourerIpc
|
||||
{
|
||||
public const string LabelRevert = "Glamourer.Revert";
|
||||
public const string LabelRevertCharacter = "Glamourer.RevertCharacter";
|
||||
public const string LabelRevertLock = "Glamourer.RevertLock";
|
||||
public const string LabelRevertCharacterLock = "Glamourer.RevertCharacterLock";
|
||||
public const string LabelRevertToAutomation = "Glamourer.RevertToAutomation";
|
||||
public const string LabelRevertToAutomationCharacter = "Glamourer.RevertToAutomationCharacter";
|
||||
public const string LabelUnlock = "Glamourer.Unlock";
|
||||
public const string LabelUnlockName = "Glamourer.UnlockName";
|
||||
|
||||
private readonly ActionProvider<string> _revertProvider;
|
||||
private readonly ActionProvider<Character?> _revertCharacterProvider;
|
||||
|
||||
private readonly ActionProvider<string, uint> _revertProviderLock;
|
||||
private readonly ActionProvider<Character?, uint> _revertCharacterProviderLock;
|
||||
|
||||
private readonly FuncProvider<string, uint, bool> _revertToAutomationProvider;
|
||||
private readonly FuncProvider<Character?, uint, bool> _revertToAutomationCharacterProvider;
|
||||
|
||||
private readonly FuncProvider<string, uint, bool> _unlockNameProvider;
|
||||
private readonly FuncProvider<Character?, uint, bool> _unlockProvider;
|
||||
|
||||
public static ActionSubscriber<string> RevertSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelRevert);
|
||||
|
||||
public static ActionSubscriber<Character?> RevertCharacterSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelRevertCharacter);
|
||||
|
||||
public static ActionSubscriber<string> RevertLockSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelRevertLock);
|
||||
|
||||
public static ActionSubscriber<Character?> RevertCharacterLockSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelRevertCharacterLock);
|
||||
|
||||
public static FuncSubscriber<string, uint, bool> UnlockNameSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelUnlockName);
|
||||
|
||||
public static FuncSubscriber<Character?, uint, bool> UnlockSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelUnlock);
|
||||
|
||||
public static FuncSubscriber<string, uint, bool> RevertToAutomationSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelRevertToAutomation);
|
||||
|
||||
public static FuncSubscriber<Character?, uint, bool> RevertToAutomationCharacterSubscriber(DalamudPluginInterface pi)
|
||||
=> new(pi, LabelRevertToAutomationCharacter);
|
||||
|
||||
public void Revert(string characterName)
|
||||
=> Revert(FindActorsRevert(characterName), 0);
|
||||
|
||||
public void RevertCharacter(Character? character)
|
||||
=> Revert(FindActors(character), 0);
|
||||
|
||||
public void RevertLock(string characterName, uint lockCode)
|
||||
=> Revert(FindActorsRevert(characterName), lockCode);
|
||||
|
||||
public void RevertCharacterLock(Character? character, uint lockCode)
|
||||
=> Revert(FindActors(character), lockCode);
|
||||
|
||||
public bool Unlock(string characterName, uint lockCode)
|
||||
=> Unlock(FindActorsRevert(characterName), lockCode);
|
||||
|
||||
public bool Unlock(Character? character, uint lockCode)
|
||||
=> Unlock(FindActors(character), lockCode);
|
||||
|
||||
public bool RevertToAutomation(string characterName, uint lockCode)
|
||||
=> RevertToAutomation(FindActorsRevert(characterName), lockCode);
|
||||
|
||||
public bool RevertToAutomation(Character? character, uint lockCode)
|
||||
=> RevertToAutomation(FindActors(character), lockCode);
|
||||
|
||||
private void Revert(IEnumerable<ActorIdentifier> actors, uint lockCode)
|
||||
{
|
||||
foreach (var id in actors)
|
||||
{
|
||||
if (_stateManager.TryGetValue(id, out var state))
|
||||
_stateManager.ResetState(state, StateChanged.Source.Ipc, lockCode);
|
||||
}
|
||||
}
|
||||
|
||||
private bool Unlock(IEnumerable<ActorIdentifier> actors, uint lockCode)
|
||||
{
|
||||
var ret = false;
|
||||
foreach (var id in actors)
|
||||
{
|
||||
if (_stateManager.TryGetValue(id, out var state))
|
||||
ret |= state.Unlock(lockCode);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool RevertToAutomation(IEnumerable<ActorIdentifier> actors, uint lockCode)
|
||||
{
|
||||
var ret = false;
|
||||
foreach (var id in actors)
|
||||
{
|
||||
if (_stateManager.TryGetValue(id, out var state))
|
||||
{
|
||||
ret |= state.Unlock(lockCode);
|
||||
if (_objects.TryGetValue(id, out var data))
|
||||
foreach (var obj in data.Objects)
|
||||
{
|
||||
_autoDesignApplier.ReapplyAutomation(obj, state.Identifier, state);
|
||||
_stateManager.ReapplyState(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Glamourer.Automation;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.State;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public partial class GlamourerIpc : IDisposable
|
||||
{
|
||||
public const int CurrentApiVersionMajor = 0;
|
||||
public const int CurrentApiVersionMinor = 4;
|
||||
|
||||
private readonly StateManager _stateManager;
|
||||
private readonly ObjectManager _objects;
|
||||
private readonly ActorService _actors;
|
||||
private readonly DesignConverter _designConverter;
|
||||
private readonly AutoDesignApplier _autoDesignApplier;
|
||||
|
||||
public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors,
|
||||
DesignConverter designConverter, StateChanged stateChangedEvent, GPoseService gPose, AutoDesignApplier autoDesignApplier)
|
||||
{
|
||||
_stateManager = stateManager;
|
||||
_objects = objects;
|
||||
_actors = actors;
|
||||
_designConverter = designConverter;
|
||||
_autoDesignApplier = autoDesignApplier;
|
||||
_gPose = gPose;
|
||||
_stateChangedEvent = stateChangedEvent;
|
||||
_apiVersionProvider = new FuncProvider<int>(pi, LabelApiVersion, ApiVersion);
|
||||
_apiVersionsProvider = new FuncProvider<(int Major, int Minor)>(pi, LabelApiVersions, ApiVersions);
|
||||
|
||||
_getAllCustomizationProvider = new FuncProvider<string, string?>(pi, LabelGetAllCustomization, GetAllCustomization);
|
||||
_getAllCustomizationFromCharacterProvider =
|
||||
new FuncProvider<Character?, string?>(pi, LabelGetAllCustomizationFromCharacter, GetAllCustomizationFromCharacter);
|
||||
|
||||
_applyAllProvider = new ActionProvider<string, string>(pi, LabelApplyAll, ApplyAll);
|
||||
_applyAllToCharacterProvider = new ActionProvider<string, Character?>(pi, LabelApplyAllToCharacter, ApplyAllToCharacter);
|
||||
_applyOnlyEquipmentProvider = new ActionProvider<string, string>(pi, LabelApplyOnlyEquipment, ApplyOnlyEquipment);
|
||||
_applyOnlyEquipmentToCharacterProvider =
|
||||
new ActionProvider<string, Character?>(pi, LabelApplyOnlyEquipmentToCharacter, ApplyOnlyEquipmentToCharacter);
|
||||
_applyOnlyCustomizationProvider = new ActionProvider<string, string>(pi, LabelApplyOnlyCustomization, ApplyOnlyCustomization);
|
||||
_applyOnlyCustomizationToCharacterProvider =
|
||||
new ActionProvider<string, Character?>(pi, LabelApplyOnlyCustomizationToCharacter, ApplyOnlyCustomizationToCharacter);
|
||||
|
||||
_applyAllProviderLock = new ActionProvider<string, string, uint>(pi, LabelApplyAllLock, ApplyAllLock);
|
||||
_applyAllToCharacterProviderLock =
|
||||
new ActionProvider<string, Character?, uint>(pi, LabelApplyAllToCharacterLock, ApplyAllToCharacterLock);
|
||||
_applyOnlyEquipmentProviderLock = new ActionProvider<string, string, uint>(pi, LabelApplyOnlyEquipmentLock, ApplyOnlyEquipmentLock);
|
||||
_applyOnlyEquipmentToCharacterProviderLock =
|
||||
new ActionProvider<string, Character?, uint>(pi, LabelApplyOnlyEquipmentToCharacterLock, ApplyOnlyEquipmentToCharacterLock);
|
||||
_applyOnlyCustomizationProviderLock =
|
||||
new ActionProvider<string, string, uint>(pi, LabelApplyOnlyCustomizationLock, ApplyOnlyCustomizationLock);
|
||||
_applyOnlyCustomizationToCharacterProviderLock =
|
||||
new ActionProvider<string, Character?, uint>(pi, LabelApplyOnlyCustomizationToCharacterLock, ApplyOnlyCustomizationToCharacterLock);
|
||||
|
||||
_revertProvider = new ActionProvider<string>(pi, LabelRevert, Revert);
|
||||
_revertCharacterProvider = new ActionProvider<Character?>(pi, LabelRevertCharacter, RevertCharacter);
|
||||
_revertProviderLock = new ActionProvider<string, uint>(pi, LabelRevertLock, RevertLock);
|
||||
_revertCharacterProviderLock = new ActionProvider<Character?, uint>(pi, LabelRevertCharacterLock, RevertCharacterLock);
|
||||
_unlockNameProvider = new FuncProvider<string, uint, bool>(pi, LabelUnlockName, Unlock);
|
||||
_unlockProvider = new FuncProvider<Character?, uint, bool>(pi, LabelUnlock, Unlock);
|
||||
_revertToAutomationProvider = new FuncProvider<string, uint, bool>(pi, LabelRevertToAutomation, RevertToAutomation);
|
||||
_revertToAutomationCharacterProvider =
|
||||
new FuncProvider<Character?, uint, bool>(pi, LabelRevertToAutomationCharacter, RevertToAutomation);
|
||||
|
||||
_stateChangedProvider = new EventProvider<StateChanged.Type, nint, Lazy<string>>(pi, LabelStateChanged);
|
||||
_gPoseChangedProvider = new EventProvider<bool>(pi, LabelGPoseChanged);
|
||||
|
||||
_stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc);
|
||||
_gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_apiVersionProvider.Dispose();
|
||||
_apiVersionsProvider.Dispose();
|
||||
|
||||
_getAllCustomizationProvider.Dispose();
|
||||
_getAllCustomizationFromCharacterProvider.Dispose();
|
||||
|
||||
_applyAllProvider.Dispose();
|
||||
_applyAllToCharacterProvider.Dispose();
|
||||
_applyOnlyEquipmentProvider.Dispose();
|
||||
_applyOnlyEquipmentToCharacterProvider.Dispose();
|
||||
_applyOnlyCustomizationProvider.Dispose();
|
||||
_applyOnlyCustomizationToCharacterProvider.Dispose();
|
||||
_applyAllProviderLock.Dispose();
|
||||
_applyAllToCharacterProviderLock.Dispose();
|
||||
_applyOnlyEquipmentProviderLock.Dispose();
|
||||
_applyOnlyEquipmentToCharacterProviderLock.Dispose();
|
||||
_applyOnlyCustomizationProviderLock.Dispose();
|
||||
_applyOnlyCustomizationToCharacterProviderLock.Dispose();
|
||||
|
||||
_revertProvider.Dispose();
|
||||
_revertCharacterProvider.Dispose();
|
||||
_revertProviderLock.Dispose();
|
||||
_revertCharacterProviderLock.Dispose();
|
||||
_unlockNameProvider.Dispose();
|
||||
_unlockProvider.Dispose();
|
||||
_revertToAutomationProvider.Dispose();
|
||||
_revertToAutomationCharacterProvider.Dispose();
|
||||
|
||||
_stateChangedEvent.Unsubscribe(OnStateChanged);
|
||||
_stateChangedProvider.Dispose();
|
||||
_gPose.Unsubscribe(OnGPoseChanged);
|
||||
_gPoseChangedProvider.Dispose();
|
||||
}
|
||||
|
||||
private IEnumerable<ActorIdentifier> FindActors(string actorName)
|
||||
{
|
||||
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
|
||||
return Array.Empty<ActorIdentifier>();
|
||||
|
||||
_objects.Update();
|
||||
return _objects.Where(i => i.Key is { IsValid: true, Type: IdentifierType.Player } && i.Key.PlayerName == byteString)
|
||||
.Select(i => i.Key);
|
||||
}
|
||||
|
||||
private IEnumerable<ActorIdentifier> FindActorsRevert(string actorName)
|
||||
{
|
||||
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
|
||||
yield break;
|
||||
|
||||
_objects.Update();
|
||||
foreach (var id in _objects.Where(i => i.Key is { IsValid: true, Type: IdentifierType.Player } && i.Key.PlayerName == byteString)
|
||||
.Select(i => i.Key))
|
||||
yield return id;
|
||||
|
||||
foreach (var id in _stateManager.Keys.Where(s => s.Type is IdentifierType.Player && s.PlayerName == byteString))
|
||||
yield return id;
|
||||
}
|
||||
|
||||
private IEnumerable<ActorIdentifier> FindActors(Character? character)
|
||||
{
|
||||
var id = _actors.AwaitedService.FromObject(character, true, true, false);
|
||||
if (!id.IsValid)
|
||||
yield break;
|
||||
|
||||
yield return id;
|
||||
}
|
||||
}
|
||||
85
Glamourer/Api/IpcProviders.cs
Normal file
85
Glamourer/Api/IpcProviders.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using Dalamud.Plugin;
|
||||
using Glamourer.Api.Api;
|
||||
using Glamourer.Api.Helpers;
|
||||
using OtterGui.Services;
|
||||
using Glamourer.Api.Enums;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public sealed class IpcProviders : IDisposable, IApiService
|
||||
{
|
||||
private readonly List<IDisposable> _providers;
|
||||
|
||||
private readonly EventProvider _disposedProvider;
|
||||
private readonly EventProvider _initializedProvider;
|
||||
|
||||
public IpcProviders(IDalamudPluginInterface pi, IGlamourerApi api)
|
||||
{
|
||||
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
|
||||
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
|
||||
_providers =
|
||||
[
|
||||
new FuncProvider<(int Major, int Minor)>(pi, "Glamourer.ApiVersions", () => api.ApiVersion), // backward compatibility
|
||||
new FuncProvider<int>(pi, "Glamourer.ApiVersion", () => api.ApiVersion.Major), // backward compatibility
|
||||
IpcSubscribers.ApiVersion.Provider(pi, api),
|
||||
IpcSubscribers.AutoReloadGearEnabled.Provider(pi, api),
|
||||
|
||||
IpcSubscribers.GetDesignList.Provider(pi, api.Designs),
|
||||
IpcSubscribers.GetDesignListExtended.Provider(pi, api.Designs),
|
||||
IpcSubscribers.GetExtendedDesignData.Provider(pi, api.Designs),
|
||||
IpcSubscribers.ApplyDesign.Provider(pi, api.Designs),
|
||||
IpcSubscribers.ApplyDesignName.Provider(pi, api.Designs),
|
||||
IpcSubscribers.AddDesign.Provider(pi, api.Designs),
|
||||
IpcSubscribers.DeleteDesign.Provider(pi, api.Designs),
|
||||
IpcSubscribers.GetDesignBase64.Provider(pi, api.Designs),
|
||||
IpcSubscribers.GetDesignJObject.Provider(pi, api.Designs),
|
||||
|
||||
IpcSubscribers.SetItem.Provider(pi, api.Items),
|
||||
IpcSubscribers.SetItemName.Provider(pi, api.Items),
|
||||
// backward compatibility
|
||||
new FuncProvider<int, byte, ulong, byte, uint, ulong, int>(pi, IpcSubscribers.Legacy.SetItemV2.Label,
|
||||
(a, b, c, d, e, f) => (int)api.Items.SetItem(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)),
|
||||
new FuncProvider<string, byte, ulong, byte, uint, ulong, int>(pi, IpcSubscribers.Legacy.SetItemName.Label,
|
||||
(a, b, c, d, e, f) => (int)api.Items.SetItemName(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)),
|
||||
IpcSubscribers.SetBonusItem.Provider(pi, api.Items),
|
||||
IpcSubscribers.SetBonusItemName.Provider(pi, api.Items),
|
||||
IpcSubscribers.SetMetaState.Provider(pi, api.Items),
|
||||
IpcSubscribers.SetMetaStateName.Provider(pi, api.Items),
|
||||
IpcSubscribers.GetState.Provider(pi, api.State),
|
||||
IpcSubscribers.GetStateName.Provider(pi, api.State),
|
||||
IpcSubscribers.GetStateBase64.Provider(pi, api.State),
|
||||
IpcSubscribers.GetStateBase64Name.Provider(pi, api.State),
|
||||
IpcSubscribers.ApplyState.Provider(pi, api.State),
|
||||
IpcSubscribers.ApplyStateName.Provider(pi, api.State),
|
||||
IpcSubscribers.ReapplyState.Provider(pi, api.State),
|
||||
IpcSubscribers.ReapplyStateName.Provider(pi, api.State),
|
||||
IpcSubscribers.RevertState.Provider(pi, api.State),
|
||||
IpcSubscribers.RevertStateName.Provider(pi, api.State),
|
||||
IpcSubscribers.UnlockState.Provider(pi, api.State),
|
||||
IpcSubscribers.CanUnlock.Provider(pi, api.State),
|
||||
IpcSubscribers.UnlockStateName.Provider(pi, api.State),
|
||||
IpcSubscribers.DeletePlayerState.Provider(pi, api.State),
|
||||
IpcSubscribers.UnlockAll.Provider(pi, api.State),
|
||||
IpcSubscribers.RevertToAutomation.Provider(pi, api.State),
|
||||
IpcSubscribers.RevertToAutomationName.Provider(pi, api.State),
|
||||
IpcSubscribers.AutoReloadGearChanged.Provider(pi, api.State),
|
||||
IpcSubscribers.StateChanged.Provider(pi, api.State),
|
||||
IpcSubscribers.StateChangedWithType.Provider(pi, api.State),
|
||||
IpcSubscribers.StateFinalized.Provider(pi, api.State),
|
||||
IpcSubscribers.GPoseChanged.Provider(pi, api.State),
|
||||
];
|
||||
_initializedProvider.Invoke();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var provider in _providers)
|
||||
provider.Dispose();
|
||||
_providers.Clear();
|
||||
_initializedProvider.Dispose();
|
||||
_disposedProvider.Invoke();
|
||||
_disposedProvider.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
217
Glamourer/Api/ItemsApi.cs
Normal file
217
Glamourer/Api/ItemsApi.cs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
using Glamourer.Api.Api;
|
||||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.State;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager stateManager) : IGlamourerApiItems, IApiService
|
||||
{
|
||||
public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stains, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags);
|
||||
if (!ResolveItem(slot, itemId, out var item))
|
||||
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
|
||||
|
||||
if (helpers.FindState(objectIndex) is not { } state)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!state.ModelData.IsHuman)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
|
||||
stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
return GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
public GlamourerApiEc SetItemName(string playerName, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stains, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags);
|
||||
if (!ResolveItem(slot, itemId, out var item))
|
||||
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
|
||||
|
||||
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
|
||||
var anyHuman = false;
|
||||
var anyFound = false;
|
||||
var anyUnlocked = false;
|
||||
foreach (var state in helpers.FindStates(playerName))
|
||||
{
|
||||
anyFound = true;
|
||||
if (!state.ModelData.IsHuman)
|
||||
continue;
|
||||
|
||||
anyHuman = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyUnlocked = true;
|
||||
stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
if (!anyFound)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!anyHuman)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc SetBonusItem(int objectIndex, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags);
|
||||
if (!ResolveBonusItem(slot, bonusItemId, out var item))
|
||||
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
|
||||
|
||||
if (helpers.FindState(objectIndex) is not { } state)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!state.ModelData.IsHuman)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
|
||||
stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
return GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
public GlamourerApiEc SetBonusItemName(string playerName, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags);
|
||||
if (!ResolveBonusItem(slot, bonusItemId, out var item))
|
||||
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
|
||||
|
||||
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
|
||||
var anyHuman = false;
|
||||
var anyFound = false;
|
||||
var anyUnlocked = false;
|
||||
foreach (var state in helpers.FindStates(playerName))
|
||||
{
|
||||
anyFound = true;
|
||||
if (!state.ModelData.IsHuman)
|
||||
continue;
|
||||
|
||||
anyHuman = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyUnlocked = true;
|
||||
stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
if (!anyFound)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!anyHuman)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc SetMetaState(int objectIndex, MetaFlag types, bool newValue, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags);
|
||||
if (types == 0)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidState, args);
|
||||
|
||||
if (helpers.FindState(objectIndex) is not { } state)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!state.ModelData.IsHuman)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
// Grab MetaIndices from attached flags, and update the states.
|
||||
var indices = types.ToIndices();
|
||||
foreach (var index in indices)
|
||||
{
|
||||
stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
return GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
public GlamourerApiEc SetMetaStateName(string playerName, MetaFlag types, bool newValue, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags);
|
||||
if (types == 0)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
|
||||
|
||||
var anyHuman = false;
|
||||
var anyFound = false;
|
||||
var anyUnlocked = false;
|
||||
foreach (var state in helpers.FindStates(playerName))
|
||||
{
|
||||
anyFound = true;
|
||||
if (!state.ModelData.IsHuman)
|
||||
continue;
|
||||
|
||||
anyHuman = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyUnlocked = true;
|
||||
// update all MetaStates for this ActorState
|
||||
foreach (var index in types.ToIndices())
|
||||
{
|
||||
stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyFound)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!anyHuman)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
private bool ResolveItem(ApiEquipSlot apiSlot, ulong itemId, out EquipItem item)
|
||||
{
|
||||
var id = (CustomItemId)itemId;
|
||||
var slot = (EquipSlot)apiSlot;
|
||||
if (id.Id == 0)
|
||||
id = ItemManager.NothingId(slot);
|
||||
|
||||
item = itemManager.Resolve(slot, id);
|
||||
return item.Valid;
|
||||
}
|
||||
|
||||
private bool ResolveBonusItem(ApiBonusSlot apiSlot, ulong itemId, out EquipItem item)
|
||||
{
|
||||
var slot = apiSlot switch
|
||||
{
|
||||
ApiBonusSlot.Glasses => BonusItemFlag.Glasses,
|
||||
_ => BonusItemFlag.Unknown,
|
||||
};
|
||||
|
||||
return itemManager.IsBonusItemValid(slot, (BonusItemId)itemId, out item);
|
||||
}
|
||||
}
|
||||
452
Glamourer/Api/StateApi.cs
Normal file
452
Glamourer/Api/StateApi.cs
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
using Glamourer.Api.Api;
|
||||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Automation;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Designs.History;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.State;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
using StateChanged = Glamourer.Events.StateChanged;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
|
||||
{
|
||||
private readonly ApiHelpers _helpers;
|
||||
private readonly StateManager _stateManager;
|
||||
private readonly DesignConverter _converter;
|
||||
private readonly AutoDesignApplier _autoDesigns;
|
||||
private readonly ActorObjectManager _objects;
|
||||
private readonly AutoRedrawChanged _autoRedraw;
|
||||
private readonly StateChanged _stateChanged;
|
||||
private readonly StateFinalized _stateFinalized;
|
||||
private readonly GPoseService _gPose;
|
||||
|
||||
public StateApi(ApiHelpers helpers,
|
||||
StateManager stateManager,
|
||||
DesignConverter converter,
|
||||
AutoDesignApplier autoDesigns,
|
||||
ActorObjectManager objects,
|
||||
AutoRedrawChanged autoRedraw,
|
||||
StateChanged stateChanged,
|
||||
StateFinalized stateFinalized,
|
||||
GPoseService gPose)
|
||||
{
|
||||
_helpers = helpers;
|
||||
_stateManager = stateManager;
|
||||
_converter = converter;
|
||||
_autoDesigns = autoDesigns;
|
||||
_objects = objects;
|
||||
_autoRedraw = autoRedraw;
|
||||
_stateChanged = stateChanged;
|
||||
_stateFinalized = stateFinalized;
|
||||
_gPose = gPose;
|
||||
_autoRedraw.Subscribe(OnAutoRedrawChange, AutoRedrawChanged.Priority.StateApi);
|
||||
_stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc);
|
||||
_stateFinalized.Subscribe(OnStateFinalized, Events.StateFinalized.Priority.StateApi);
|
||||
_gPose.Subscribe(OnGPoseChange, GPoseService.Priority.StateApi);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_autoRedraw.Unsubscribe(OnAutoRedrawChange);
|
||||
_stateChanged.Unsubscribe(OnStateChanged);
|
||||
_stateFinalized.Unsubscribe(OnStateFinalized);
|
||||
_gPose.Unsubscribe(OnGPoseChange);
|
||||
}
|
||||
|
||||
public (GlamourerApiEc, JObject?) GetState(int objectIndex, uint key)
|
||||
=> Convert(_helpers.FindState(objectIndex), key);
|
||||
|
||||
public (GlamourerApiEc, JObject?) GetStateName(string playerName, uint key)
|
||||
=> Convert(_helpers.FindStates(playerName).FirstOrDefault(), key);
|
||||
|
||||
public (GlamourerApiEc, string?) GetStateBase64(int objectIndex, uint key)
|
||||
=> ConvertBase64(_helpers.FindState(objectIndex), key);
|
||||
|
||||
public (GlamourerApiEc, string?) GetStateBase64Name(string objectName, uint key)
|
||||
=> ConvertBase64(_helpers.FindStates(objectName).FirstOrDefault(), key);
|
||||
|
||||
public GlamourerApiEc ApplyState(object applyState, int objectIndex, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
|
||||
if (Convert(applyState, flags, out var version) is not { } design)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidState, args);
|
||||
|
||||
if (_helpers.FindState(objectIndex) is not { } state)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
if (version < 3 && state.ModelData.ModelId != 0)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
ApplyDesign(state, design, key, flags);
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc ApplyStateName(object applyState, string playerName, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
|
||||
if (Convert(applyState, flags, out var version) is not { } design)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidState, args);
|
||||
|
||||
var states = _helpers.FindExistingStates(playerName);
|
||||
|
||||
var any = false;
|
||||
var anyUnlocked = false;
|
||||
var anyHuman = false;
|
||||
foreach (var state in states)
|
||||
{
|
||||
any = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyUnlocked = true;
|
||||
if (version < 3 && state.ModelData.ModelId != 0)
|
||||
continue;
|
||||
|
||||
anyHuman = true;
|
||||
ApplyDesign(state, design, key, flags);
|
||||
}
|
||||
|
||||
if (any)
|
||||
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
if (!anyHuman)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc ReapplyState(int objectIndex, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
|
||||
if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (state is null)
|
||||
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
Reapply(_objects.Objects[objectIndex], state, key, flags);
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc ReapplyStateName(string playerName, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
|
||||
var states = _helpers.FindExistingStates(playerName);
|
||||
|
||||
var any = false;
|
||||
var anyReapplied = false;
|
||||
foreach (var state in states)
|
||||
{
|
||||
any = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyReapplied = true;
|
||||
anyReapplied |= Reapply(state, key, flags) is GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
if (any)
|
||||
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!anyReapplied)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc RevertState(int objectIndex, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
|
||||
if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (state == null)
|
||||
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
Revert(state, key, flags);
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc RevertStateName(string playerName, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
|
||||
var states = _helpers.FindExistingStates(playerName);
|
||||
|
||||
var any = false;
|
||||
var anyUnlocked = false;
|
||||
foreach (var state in states)
|
||||
{
|
||||
any = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyUnlocked = true;
|
||||
Revert(state, key, flags);
|
||||
}
|
||||
|
||||
if (any)
|
||||
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc UnlockState(int objectIndex, uint key)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Key", key);
|
||||
if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (state == null)
|
||||
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!state.Unlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc CanUnlock(int objectIndex, uint key, out bool isLocked, out bool canUnlock)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Key", key);
|
||||
isLocked = false;
|
||||
canUnlock = true;
|
||||
if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
if (state is null)
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
isLocked = state.IsLocked;
|
||||
canUnlock = state.CanUnlock(key);
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc UnlockStateName(string playerName, uint key)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "Key", key);
|
||||
var states = _helpers.FindExistingStates(playerName);
|
||||
|
||||
var any = false;
|
||||
var anyUnlocked = false;
|
||||
foreach (var state in states)
|
||||
{
|
||||
any = true;
|
||||
anyUnlocked |= state.Unlock(key);
|
||||
}
|
||||
|
||||
if (any)
|
||||
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc DeletePlayerState(string playerName, ushort worldId, uint key)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "World", worldId, "Key", key);
|
||||
var states = _helpers.FindExistingStates(playerName).ToList();
|
||||
if (states.Count is 0)
|
||||
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
var anyLocked = false;
|
||||
foreach (var state in states)
|
||||
{
|
||||
if (state.CanUnlock(key))
|
||||
_stateManager.DeleteState(state.Identifier);
|
||||
else
|
||||
anyLocked = true;
|
||||
}
|
||||
|
||||
return ApiHelpers.Return(anyLocked
|
||||
? GlamourerApiEc.InvalidKey
|
||||
: GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public int UnlockAll(uint key)
|
||||
=> _stateManager.Values.Count(state => state.Unlock(key));
|
||||
|
||||
public GlamourerApiEc RevertToAutomation(int objectIndex, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
|
||||
if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success)
|
||||
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (state == null)
|
||||
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
RevertToAutomation(_objects.Objects[objectIndex], state, key, flags);
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public GlamourerApiEc RevertToAutomationName(string playerName, uint key, ApplyFlag flags)
|
||||
{
|
||||
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
|
||||
var states = _helpers.FindExistingStates(playerName);
|
||||
|
||||
var any = false;
|
||||
var anyUnlocked = false;
|
||||
var anyReverted = false;
|
||||
foreach (var state in states)
|
||||
{
|
||||
any = true;
|
||||
if (!state.CanUnlock(key))
|
||||
continue;
|
||||
|
||||
anyUnlocked = true;
|
||||
anyReverted |= RevertToAutomation(state, key, flags) is GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
if (any)
|
||||
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
|
||||
|
||||
if (!anyReverted)
|
||||
ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
|
||||
|
||||
if (!anyUnlocked)
|
||||
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
|
||||
|
||||
return ApiHelpers.Return(GlamourerApiEc.Success, args);
|
||||
}
|
||||
|
||||
public event Action<bool>? AutoReloadGearChanged;
|
||||
public event Action<nint>? StateChanged;
|
||||
public event Action<IntPtr, StateChangeType>? StateChangedWithType;
|
||||
public event Action<IntPtr, StateFinalizationType>? StateFinalized;
|
||||
public event Action<bool>? GPoseChanged;
|
||||
|
||||
private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags)
|
||||
{
|
||||
var once = (flags & ApplyFlag.Once) != 0;
|
||||
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true,
|
||||
ResetMaterials: !once && key != 0, IsFinal: true);
|
||||
_stateManager.ApplyDesign(state, design, settings);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
private GlamourerApiEc Reapply(ActorState state, uint key, ApplyFlag flags)
|
||||
{
|
||||
if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid)
|
||||
return GlamourerApiEc.ActorNotFound;
|
||||
|
||||
foreach (var actor in actors.Objects)
|
||||
Reapply(actor, state, key, flags);
|
||||
|
||||
return GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
private void Reapply(Actor actor, ActorState state, uint key, ApplyFlag flags)
|
||||
{
|
||||
var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual;
|
||||
_stateManager.ReapplyState(actor, state, false, source, true);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
private void Revert(ActorState state, uint key, ApplyFlag flags)
|
||||
{
|
||||
var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual;
|
||||
switch (flags & (ApplyFlag.Equipment | ApplyFlag.Customization))
|
||||
{
|
||||
case ApplyFlag.Equipment: _stateManager.ResetEquip(state, source, key); break;
|
||||
case ApplyFlag.Customization: _stateManager.ResetCustomize(state, source, key); break;
|
||||
case ApplyFlag.Equipment | ApplyFlag.Customization: _stateManager.ResetState(state, source, key, true); break;
|
||||
}
|
||||
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
private GlamourerApiEc RevertToAutomation(ActorState state, uint key, ApplyFlag flags)
|
||||
{
|
||||
if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid)
|
||||
return GlamourerApiEc.ActorNotFound;
|
||||
|
||||
foreach (var actor in actors.Objects)
|
||||
RevertToAutomation(actor, state, key, flags);
|
||||
|
||||
return GlamourerApiEc.Success;
|
||||
}
|
||||
|
||||
private void RevertToAutomation(Actor actor, ActorState state, uint key, ApplyFlag flags)
|
||||
{
|
||||
var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed;
|
||||
_autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, false, out var forcedRedraw);
|
||||
_stateManager.ReapplyAutomationState(actor, state, forcedRedraw, true, source);
|
||||
ApiHelpers.Lock(state, key, flags);
|
||||
}
|
||||
|
||||
private (GlamourerApiEc, JObject?) Convert(ActorState? state, uint key)
|
||||
{
|
||||
if (state == null)
|
||||
return (GlamourerApiEc.ActorNotFound, null);
|
||||
|
||||
if (!state.CanUnlock(key))
|
||||
return (GlamourerApiEc.InvalidKey, null);
|
||||
|
||||
return (GlamourerApiEc.Success, _converter.ShareJObject(state, ApplicationRules.All));
|
||||
}
|
||||
|
||||
private (GlamourerApiEc, string?) ConvertBase64(ActorState? state, uint key)
|
||||
{
|
||||
var (ec, jObj) = Convert(state, key);
|
||||
return (ec, jObj != null ? DesignConverter.ToBase64(jObj) : null);
|
||||
}
|
||||
|
||||
private DesignBase? Convert(object? state, ApplyFlag flags, out byte version)
|
||||
{
|
||||
version = DesignConverter.Version;
|
||||
return state switch
|
||||
{
|
||||
string s => _converter.FromBase64(s, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0, out version),
|
||||
JObject j => _converter.FromJObject(j, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private void OnAutoRedrawChange(bool autoReload)
|
||||
=> AutoReloadGearChanged?.Invoke(autoReload);
|
||||
|
||||
private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5)
|
||||
{
|
||||
Glamourer.Log.Excessive($"[OnStateChanged] State Changed with Type {type} [Affecting {actors.ToLazyString("nothing")}.]");
|
||||
if (StateChanged != null)
|
||||
foreach (var actor in actors.Objects)
|
||||
StateChanged.Invoke(actor.Address);
|
||||
|
||||
if (StateChangedWithType != null)
|
||||
foreach (var actor in actors.Objects)
|
||||
StateChangedWithType.Invoke(actor.Address, type);
|
||||
}
|
||||
|
||||
private void OnStateFinalized(StateFinalizationType type, ActorData actors)
|
||||
{
|
||||
Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]");
|
||||
if (StateFinalized != null)
|
||||
foreach (var actor in actors.Objects)
|
||||
StateFinalized.Invoke(actor.Address, type);
|
||||
}
|
||||
|
||||
private void OnGPoseChange(bool gPose)
|
||||
=> GPoseChanged?.Invoke(gPose);
|
||||
}
|
||||
74
Glamourer/Automation/ApplicationType.cs
Normal file
74
Glamourer/Automation/ApplicationType.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.GameData;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Automation;
|
||||
|
||||
[Flags]
|
||||
public enum ApplicationType : byte
|
||||
{
|
||||
Armor = 0x01,
|
||||
Customizations = 0x02,
|
||||
Weapons = 0x04,
|
||||
GearCustomization = 0x08,
|
||||
Accessories = 0x10,
|
||||
|
||||
All = Armor | Accessories | Customizations | Weapons | GearCustomization,
|
||||
}
|
||||
|
||||
public static class ApplicationTypeExtensions
|
||||
{
|
||||
public static readonly IReadOnlyList<(ApplicationType, string)> Types =
|
||||
[
|
||||
(ApplicationType.Customizations,
|
||||
"Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."),
|
||||
(ApplicationType.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."),
|
||||
(ApplicationType.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."),
|
||||
(ApplicationType.GearCustomization, "Apply all dye and crest changes that are enabled in this design."),
|
||||
(ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."),
|
||||
];
|
||||
|
||||
public static ApplicationCollection Collection(this ApplicationType type)
|
||||
{
|
||||
var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0)
|
||||
| (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0)
|
||||
| (type.HasFlag(ApplicationType.Accessories) ? AccessoryFlags : 0)
|
||||
| (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0);
|
||||
var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0;
|
||||
var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0;
|
||||
var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
|
||||
var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.EarState : 0)
|
||||
| (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0)
|
||||
| (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0);
|
||||
var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0;
|
||||
|
||||
return new ApplicationCollection(equipFlags, bonusFlags, customizeFlags, crestFlags, parameterFlags, metaFlags);
|
||||
}
|
||||
|
||||
public static ApplicationCollection ApplyWhat(this ApplicationType type, IDesignStandIn designStandIn)
|
||||
{
|
||||
if(designStandIn is not DesignBase design)
|
||||
return type.Collection();
|
||||
var ret = type.Collection().Restrict(design.Application);
|
||||
ret.CustomizeRaw = ret.CustomizeRaw.FixApplication(design.CustomizeSet);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;
|
||||
public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet;
|
||||
public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger;
|
||||
|
||||
public const EquipFlag StainFlags = EquipFlag.MainhandStain
|
||||
| EquipFlag.OffhandStain
|
||||
| EquipFlag.HeadStain
|
||||
| EquipFlag.BodyStain
|
||||
| EquipFlag.HandsStain
|
||||
| EquipFlag.LegsStain
|
||||
| EquipFlag.FeetStain
|
||||
| EquipFlag.EarsStain
|
||||
| EquipFlag.NeckStain
|
||||
| EquipFlag.WristStain
|
||||
| EquipFlag.RFingerStain
|
||||
| EquipFlag.LFingerStain;
|
||||
}
|
||||
|
|
@ -1,102 +1,66 @@
|
|||
using System;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Interop.Structs;
|
||||
using Glamourer.State;
|
||||
using Glamourer.Structs;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Designs.Special;
|
||||
using Glamourer.GameData;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Automation;
|
||||
|
||||
public class AutoDesign
|
||||
{
|
||||
public const string RevertName = "Revert";
|
||||
|
||||
[Flags]
|
||||
public enum Type : byte
|
||||
{
|
||||
Armor = 0x01,
|
||||
Customizations = 0x02,
|
||||
Weapons = 0x04,
|
||||
Stains = 0x08,
|
||||
Accessories = 0x10,
|
||||
|
||||
All = Armor | Accessories | Customizations | Weapons | Stains,
|
||||
}
|
||||
|
||||
public Design? Design;
|
||||
public JobGroup Jobs;
|
||||
public Type ApplicationType;
|
||||
|
||||
public string Name(bool incognito)
|
||||
=> Revert ? RevertName : incognito ? Design!.Incognito : Design!.Name.Text;
|
||||
|
||||
public ref DesignData GetDesignData(ActorState state)
|
||||
=> ref Design == null ? ref state.BaseData : ref Design.DesignData;
|
||||
|
||||
public bool Revert
|
||||
=> Design == null;
|
||||
public IDesignStandIn Design = new RevertDesign();
|
||||
public JobGroup Jobs;
|
||||
public ApplicationType Type;
|
||||
public short GearsetIndex = -1;
|
||||
|
||||
public AutoDesign Clone()
|
||||
=> new()
|
||||
{
|
||||
Design = Design,
|
||||
ApplicationType = ApplicationType,
|
||||
Jobs = Jobs,
|
||||
Design = Design,
|
||||
Type = Type,
|
||||
Jobs = Jobs,
|
||||
GearsetIndex = GearsetIndex,
|
||||
};
|
||||
|
||||
public unsafe bool IsActive(Actor actor)
|
||||
=> actor.IsCharacter && Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob);
|
||||
|
||||
public JObject Serialize()
|
||||
=> new()
|
||||
{
|
||||
["Design"] = Design?.Identifier.ToString(),
|
||||
["ApplicationType"] = (uint)ApplicationType,
|
||||
["Conditions"] = CreateConditionObject(),
|
||||
};
|
||||
|
||||
private JObject CreateConditionObject()
|
||||
{
|
||||
var ret = new JObject();
|
||||
if (Jobs.Id != 0)
|
||||
ret["JobGroup"] = Jobs.Id;
|
||||
if (!actor.IsCharacter)
|
||||
return false;
|
||||
|
||||
var ret = true;
|
||||
if (GearsetIndex < 0)
|
||||
ret &= Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob);
|
||||
else
|
||||
ret &= AutoDesignApplier.CheckGearset(GearsetIndex);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public (EquipFlag Equip, CustomizeFlag Customize, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat()
|
||||
public JObject Serialize()
|
||||
{
|
||||
var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0)
|
||||
| (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0)
|
||||
| (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0)
|
||||
| (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0);
|
||||
var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0;
|
||||
|
||||
if (Revert)
|
||||
return (equipFlags, customizeFlags, ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Armor),
|
||||
ApplicationType.HasFlag(Type.Weapons), ApplicationType.HasFlag(Type.Customizations));
|
||||
|
||||
return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize,
|
||||
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(),
|
||||
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(),
|
||||
ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(),
|
||||
ApplicationType.HasFlag(Type.Customizations) && Design.DoApplyWetness());
|
||||
var ret = new JObject
|
||||
{
|
||||
["Design"] = Design.SerializeName(),
|
||||
["Type"] = (uint)Type,
|
||||
["Conditions"] = CreateConditionObject(),
|
||||
};
|
||||
Design.AddData(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;
|
||||
public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet;
|
||||
public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger;
|
||||
private JObject CreateConditionObject()
|
||||
{
|
||||
var ret = new JObject
|
||||
{
|
||||
["Gearset"] = GearsetIndex,
|
||||
["JobGroup"] = Jobs.Id.Id,
|
||||
};
|
||||
|
||||
public const EquipFlag StainFlags = EquipFlag.MainhandStain
|
||||
| EquipFlag.OffhandStain
|
||||
| EquipFlag.HeadStain
|
||||
| EquipFlag.BodyStain
|
||||
| EquipFlag.HandsStain
|
||||
| EquipFlag.LegsStain
|
||||
| EquipFlag.FeetStain
|
||||
| EquipFlag.EarsStain
|
||||
| EquipFlag.NeckStain
|
||||
| EquipFlag.WristStain
|
||||
| EquipFlag.RFingerStain
|
||||
| EquipFlag.LFingerStain;
|
||||
return ret;
|
||||
}
|
||||
|
||||
public ApplicationCollection ApplyWhat()
|
||||
=> Type.ApplyWhat(Design);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,114 +1,118 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Glamourer.Customization;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Designs.Links;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Interop.Structs;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.State;
|
||||
using Glamourer.Structs;
|
||||
using Glamourer.Unlocks;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Automation;
|
||||
|
||||
public class AutoDesignApplier : IDisposable
|
||||
public sealed class AutoDesignApplier : IDisposable
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly AutoDesignManager _manager;
|
||||
private readonly StateManager _state;
|
||||
private readonly JobService _jobs;
|
||||
private readonly ActorService _actors;
|
||||
private readonly CustomizationService _customizations;
|
||||
private readonly CustomizeUnlockManager _customizeUnlocks;
|
||||
private readonly ItemUnlockManager _itemUnlocks;
|
||||
private readonly AutomationChanged _event;
|
||||
private readonly ObjectManager _objects;
|
||||
private readonly WeaponLoading _weapons;
|
||||
private readonly HumanModelList _humans;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly Configuration _config;
|
||||
private readonly AutoDesignManager _manager;
|
||||
private readonly StateManager _state;
|
||||
private readonly JobService _jobs;
|
||||
private readonly EquippedGearset _equippedGearset;
|
||||
private readonly ActorManager _actors;
|
||||
private readonly AutomationChanged _event;
|
||||
private readonly ActorObjectManager _objects;
|
||||
private readonly WeaponLoading _weapons;
|
||||
private readonly HumanModelList _humans;
|
||||
private readonly DesignMerger _designMerger;
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private ActorState? _jobChangeState;
|
||||
private readonly Dictionary<FullEquipType, (EquipItem, StateChanged.Source)> _jobChangeMainhand = new();
|
||||
private readonly Dictionary<FullEquipType, (EquipItem, StateChanged.Source)> _jobChangeOffhand = new();
|
||||
private readonly JobChangeState _jobChangeState;
|
||||
|
||||
private void ResetJobChange()
|
||||
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors,
|
||||
AutomationChanged @event, ActorObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState,
|
||||
EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState)
|
||||
{
|
||||
_jobChangeState = null;
|
||||
_jobChangeMainhand.Clear();
|
||||
_jobChangeOffhand.Clear();
|
||||
}
|
||||
|
||||
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs,
|
||||
CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
|
||||
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState)
|
||||
{
|
||||
_config = config;
|
||||
_manager = manager;
|
||||
_state = state;
|
||||
_jobs = jobs;
|
||||
_customizations = customizations;
|
||||
_actors = actors;
|
||||
_itemUnlocks = itemUnlocks;
|
||||
_customizeUnlocks = customizeUnlocks;
|
||||
_event = @event;
|
||||
_objects = objects;
|
||||
_weapons = weapons;
|
||||
_humans = humans;
|
||||
_clientState = clientState;
|
||||
_jobs.JobChanged += OnJobChange;
|
||||
_config = config;
|
||||
_manager = manager;
|
||||
_state = state;
|
||||
_jobs = jobs;
|
||||
_actors = actors;
|
||||
_event = @event;
|
||||
_objects = objects;
|
||||
_weapons = weapons;
|
||||
_humans = humans;
|
||||
_clientState = clientState;
|
||||
_equippedGearset = equippedGearset;
|
||||
_designMerger = designMerger;
|
||||
_jobChangeState = jobChangeState;
|
||||
_jobs.JobChanged += OnJobChange;
|
||||
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier);
|
||||
_weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier);
|
||||
_equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier);
|
||||
}
|
||||
|
||||
public void OnEnableAutoDesignsChanged(bool value)
|
||||
{
|
||||
if (value)
|
||||
return;
|
||||
|
||||
foreach (var state in _state.Values)
|
||||
state.Sources.RemoveFixedDesignSources();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_weapons.Unsubscribe(OnWeaponLoading);
|
||||
_event.Unsubscribe(OnAutomationChange);
|
||||
_equippedGearset.Unsubscribe(OnEquippedGearset);
|
||||
_jobs.JobChanged -= OnJobChange;
|
||||
}
|
||||
|
||||
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
|
||||
private void OnWeaponLoading(Actor actor, EquipSlot slot, ref CharacterWeapon weapon)
|
||||
{
|
||||
if (_jobChangeState == null || !_config.EnableAutoDesigns)
|
||||
if (!_jobChangeState.HasState || !_config.EnableAutoDesigns)
|
||||
return;
|
||||
|
||||
var id = actor.GetIdentifier(_actors.AwaitedService);
|
||||
var id = actor.GetIdentifier(_actors);
|
||||
if (id == _jobChangeState.Identifier)
|
||||
{
|
||||
var current = _jobChangeState.BaseData.Item(slot);
|
||||
if (slot is EquipSlot.MainHand)
|
||||
var state = _jobChangeState.State!;
|
||||
var current = state.BaseData.Item(slot);
|
||||
switch (slot)
|
||||
{
|
||||
if (_jobChangeMainhand.TryGetValue(current.Type, out var data))
|
||||
case EquipSlot.MainHand:
|
||||
{
|
||||
Glamourer.Log.Verbose($"Changing Mainhand from {_jobChangeState.ModelData.Weapon(EquipSlot.MainHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}.");
|
||||
_state.ChangeItem(_jobChangeState, EquipSlot.MainHand, data.Item1, data.Item2);
|
||||
weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.MainHand);
|
||||
}
|
||||
}
|
||||
else if (slot is EquipSlot.OffHand && current.Type == _jobChangeState.BaseData.MainhandType.Offhand())
|
||||
{
|
||||
if (_jobChangeOffhand.TryGetValue(current.Type, out var data))
|
||||
{
|
||||
Glamourer.Log.Verbose($"Changing Offhand from {_jobChangeState.ModelData.Weapon(EquipSlot.OffHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}.");
|
||||
_state.ChangeItem(_jobChangeState, EquipSlot.OffHand, data.Item1, data.Item2);
|
||||
weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.OffHand);
|
||||
}
|
||||
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
|
||||
{
|
||||
Glamourer.Log.Verbose(
|
||||
$"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}.");
|
||||
_state.ChangeItem(state, EquipSlot.MainHand, data.Item1, new ApplySettings(Source: data.Item2));
|
||||
weapon = state.ModelData.Weapon(EquipSlot.MainHand);
|
||||
}
|
||||
|
||||
ResetJobChange();
|
||||
break;
|
||||
}
|
||||
case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand():
|
||||
{
|
||||
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
|
||||
{
|
||||
Glamourer.Log.Verbose(
|
||||
$"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}.");
|
||||
_state.ChangeItem(state, EquipSlot.OffHand, data.Item1, new ApplySettings(Source: data.Item2));
|
||||
weapon = state.ModelData.Weapon(EquipSlot.OffHand);
|
||||
}
|
||||
|
||||
_jobChangeState.Reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetJobChange();
|
||||
_jobChangeState.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,57 +121,6 @@ public class AutoDesignApplier : IDisposable
|
|||
if (!_config.EnableAutoDesigns || set == null)
|
||||
return;
|
||||
|
||||
void RemoveOld(ActorIdentifier[]? identifiers)
|
||||
{
|
||||
if (identifiers == null)
|
||||
return;
|
||||
|
||||
foreach (var id in identifiers)
|
||||
{
|
||||
if (id.Type is IdentifierType.Player && id.HomeWorld == WorldId.AnyWorld)
|
||||
foreach (var state in _state.Where(kvp => kvp.Key.PlayerName == id.PlayerName).Select(kvp => kvp.Value))
|
||||
state.RemoveFixedDesignSources();
|
||||
else if (_state.TryGetValue(id, out var state))
|
||||
state.RemoveFixedDesignSources();
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyNew(AutoDesignSet? newSet)
|
||||
{
|
||||
if (newSet is not { Enabled: true })
|
||||
return;
|
||||
|
||||
_objects.Update();
|
||||
foreach (var id in newSet.Identifiers)
|
||||
{
|
||||
if (_objects.TryGetValue(id, out var data))
|
||||
{
|
||||
if (_state.GetOrCreate(id, data.Objects[0], out var state))
|
||||
{
|
||||
Reduce(data.Objects[0], state, newSet, false, false);
|
||||
foreach (var actor in data.Objects)
|
||||
_state.ReapplyState(actor);
|
||||
}
|
||||
}
|
||||
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
|
||||
{
|
||||
foreach (var actor in data.Objects)
|
||||
{
|
||||
var specificId = actor.GetIdentifier(_actors.AwaitedService);
|
||||
if (_state.GetOrCreate(specificId, actor, out var state))
|
||||
{
|
||||
Reduce(actor, state, newSet, false, false);
|
||||
_state.ReapplyState(actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_state.TryGetValue(id, out var state))
|
||||
{
|
||||
state.RemoveFixedDesignSources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case AutomationChanged.Type.ToggleSet when !set.Enabled:
|
||||
|
|
@ -177,7 +130,7 @@ public class AutoDesignApplier : IDisposable
|
|||
break;
|
||||
case AutomationChanged.Type.ChangeIdentifier when set.Enabled:
|
||||
// Remove fixed state from the old identifiers assigned and the old enabled set, if any.
|
||||
var (oldIds, _, oldSet) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!;
|
||||
var (oldIds, _, _) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!;
|
||||
RemoveOld(oldIds);
|
||||
ApplyNew(set); // Does not need to disable oldSet because same identifiers.
|
||||
break;
|
||||
|
|
@ -188,24 +141,77 @@ public class AutoDesignApplier : IDisposable
|
|||
case AutomationChanged.Type.ChangedDesign:
|
||||
case AutomationChanged.Type.ChangedConditions:
|
||||
case AutomationChanged.Type.ChangedType:
|
||||
case AutomationChanged.Type.ChangedData:
|
||||
ApplyNew(set);
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void ApplyNew(AutoDesignSet? newSet)
|
||||
{
|
||||
if (newSet is not { Enabled: true })
|
||||
return;
|
||||
|
||||
foreach (var id in newSet.Identifiers)
|
||||
{
|
||||
if (_objects.TryGetValue(id, out var data))
|
||||
{
|
||||
if (_state.GetOrCreate(id, data.Objects[0], out var state))
|
||||
{
|
||||
Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
|
||||
foreach (var actor in data.Objects)
|
||||
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
|
||||
}
|
||||
}
|
||||
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
|
||||
{
|
||||
foreach (var actor in data.Objects)
|
||||
{
|
||||
var specificId = actor.GetIdentifier(_actors);
|
||||
if (_state.GetOrCreate(specificId, actor, out var state))
|
||||
{
|
||||
Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
|
||||
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_state.TryGetValue(id, out var state))
|
||||
{
|
||||
state.Sources.RemoveFixedDesignSources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveOld(ActorIdentifier[]? identifiers)
|
||||
{
|
||||
if (identifiers == null)
|
||||
return;
|
||||
|
||||
foreach (var id in identifiers)
|
||||
{
|
||||
if (id.Type is IdentifierType.Player && id.HomeWorld == WorldId.AnyWorld)
|
||||
foreach (var state in _state.Where(kvp => kvp.Key.PlayerName == id.PlayerName).Select(kvp => kvp.Value))
|
||||
state.Sources.RemoveFixedDesignSources();
|
||||
else if (_state.TryGetValue(id, out var state))
|
||||
state.Sources.RemoveFixedDesignSources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnJobChange(Actor actor, Job oldJob, Job newJob)
|
||||
{
|
||||
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id))
|
||||
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id))
|
||||
return;
|
||||
|
||||
if (!GetPlayerSet(id, out var set))
|
||||
{
|
||||
if (_state.TryGetValue(id, out var s))
|
||||
s.LastJob = (byte)newJob.Id;
|
||||
s.LastJob = newJob.Id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_state.TryGetValue(id, out var state))
|
||||
if (!_state.GetOrCreate(actor, out var state))
|
||||
return;
|
||||
|
||||
if (oldJob.Id == newJob.Id && state.LastJob == newJob.Id)
|
||||
|
|
@ -213,19 +219,21 @@ public class AutoDesignApplier : IDisposable
|
|||
|
||||
var respectManual = state.LastJob == newJob.Id;
|
||||
state.LastJob = actor.Job;
|
||||
Reduce(actor, state, set, respectManual, true);
|
||||
_state.ReapplyState(actor);
|
||||
Reduce(actor, state, set, respectManual, true, true, out var forcedRedraw);
|
||||
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
|
||||
}
|
||||
|
||||
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state)
|
||||
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, bool forcedNew, out bool forcedRedraw)
|
||||
{
|
||||
forcedRedraw = false;
|
||||
if (!_config.EnableAutoDesigns)
|
||||
return;
|
||||
|
||||
if (!GetPlayerSet(identifier, out var set))
|
||||
return;
|
||||
if (reset)
|
||||
_state.ResetState(state, StateSource.Game);
|
||||
|
||||
Reduce(actor, state, set, false, false);
|
||||
if (GetPlayerSet(identifier, out var set))
|
||||
Reduce(actor, state, set, false, false, forcedNew, out forcedRedraw);
|
||||
}
|
||||
|
||||
public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state)
|
||||
|
|
@ -233,9 +241,6 @@ public class AutoDesignApplier : IDisposable
|
|||
AutoDesignSet set;
|
||||
if (!_state.TryGetValue(identifier, out state))
|
||||
{
|
||||
if (!_config.EnableAutoDesigns)
|
||||
return false;
|
||||
|
||||
if (!GetPlayerSet(identifier, out set!))
|
||||
return false;
|
||||
|
||||
|
|
@ -245,70 +250,91 @@ public class AutoDesignApplier : IDisposable
|
|||
else if (!GetPlayerSet(identifier, out set!))
|
||||
{
|
||||
if (state.UpdateTerritory(_clientState.TerritoryType) && _config.RevertManualChangesOnZoneChange)
|
||||
_state.ResetState(state, StateChanged.Source.Game);
|
||||
_state.ResetState(state, StateSource.Game);
|
||||
return true;
|
||||
}
|
||||
|
||||
var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange;
|
||||
if (!respectManual)
|
||||
_state.ResetState(state, StateChanged.Source.Game);
|
||||
Reduce(actor, state, set, respectManual, false);
|
||||
_state.ResetState(state, StateSource.Game);
|
||||
Reduce(actor, state, set, respectManual, false, false, out _);
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange)
|
||||
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange, bool newApplication,
|
||||
out bool forcedRedraw)
|
||||
{
|
||||
EquipFlag totalEquipFlags = 0;
|
||||
CustomizeFlag totalCustomizeFlags = 0;
|
||||
byte totalMetaFlags = 0;
|
||||
if (set.BaseState == AutoDesignSet.Base.Game)
|
||||
_state.ResetStateFixed(state);
|
||||
else if (!respectManual)
|
||||
state.RemoveFixedDesignSources();
|
||||
|
||||
if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId))
|
||||
return;
|
||||
|
||||
foreach (var design in set.Designs)
|
||||
if (set.BaseState is AutoDesignSet.Base.Game)
|
||||
{
|
||||
if (!design.IsActive(actor))
|
||||
continue;
|
||||
|
||||
if (design.ApplicationType is 0)
|
||||
continue;
|
||||
|
||||
ref var data = ref design.GetDesignData(state);
|
||||
var source = design.Revert ? StateChanged.Source.Game : StateChanged.Source.Fixed;
|
||||
|
||||
if (!data.IsHuman)
|
||||
continue;
|
||||
|
||||
var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat();
|
||||
ReduceMeta(state, data, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags, respectManual, source);
|
||||
ReduceCustomize(state, data, customizeFlags, ref totalCustomizeFlags, respectManual, source);
|
||||
ReduceEquip(state, data, equipFlags, ref totalEquipFlags, respectManual, source, fromJobChange);
|
||||
_state.ResetStateFixed(state, respectManual);
|
||||
}
|
||||
else if (!respectManual)
|
||||
{
|
||||
state.Sources.RemoveFixedDesignSources();
|
||||
for (var i = 0; i < state.Materials.Values.Count; ++i)
|
||||
{
|
||||
var (key, value) = state.Materials.Values[i];
|
||||
if (value.Source is StateSource.Fixed)
|
||||
state.Materials.UpdateValue(key, new MaterialValueState(value.Game, value.Model, value.DrawData, StateSource.Manual),
|
||||
out _);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalCustomizeFlags != 0)
|
||||
state.ModelData.ModelId = 0;
|
||||
forcedRedraw = false;
|
||||
if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId))
|
||||
return;
|
||||
|
||||
if (actor.IsTransformed)
|
||||
return;
|
||||
|
||||
var mergedDesign = _designMerger.Merge(
|
||||
set.Designs.Where(d => d.IsActive(actor))
|
||||
.SelectMany(d => d.Design.AllLinks(newApplication).Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))),
|
||||
state.ModelData.Customize, state.BaseData, true, _config.AlwaysApplyAssociatedMods);
|
||||
|
||||
if (_objects.IsInGPose && actor.IsGPoseOrCutscene)
|
||||
{
|
||||
mergedDesign.ResetTemporarySettings = false;
|
||||
mergedDesign.AssociatedMods.Clear();
|
||||
}
|
||||
else if (set.ResetTemporarySettings)
|
||||
{
|
||||
mergedDesign.ResetTemporarySettings = true;
|
||||
}
|
||||
|
||||
_state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false, false, false));
|
||||
forcedRedraw = mergedDesign.ForcedRedraw;
|
||||
}
|
||||
|
||||
/// <summary> Get world-specific first and all-world afterwards. </summary>
|
||||
/// <summary> Get world-specific first and all-world afterward. </summary>
|
||||
private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set)
|
||||
{
|
||||
if (!_config.EnableAutoDesigns)
|
||||
{
|
||||
set = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (identifier.Type)
|
||||
{
|
||||
case IdentifierType.Player:
|
||||
if (_manager.EnabledSets.TryGetValue(identifier, out set))
|
||||
return true;
|
||||
|
||||
identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue);
|
||||
identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld);
|
||||
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
||||
case IdentifierType.Retainer:
|
||||
case IdentifierType.Npc:
|
||||
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
||||
case IdentifierType.Owned:
|
||||
identifier = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId);
|
||||
if (_manager.EnabledSets.TryGetValue(identifier, out set))
|
||||
return true;
|
||||
|
||||
identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId);
|
||||
if (_manager.EnabledSets.TryGetValue(identifier, out set))
|
||||
return true;
|
||||
|
||||
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
|
||||
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
||||
default:
|
||||
set = null;
|
||||
|
|
@ -316,182 +342,37 @@ public class AutoDesignApplier : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void ReduceEquip(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags, bool respectManual,
|
||||
StateChanged.Source source, bool fromJobChange)
|
||||
internal static int NewGearsetId = -1;
|
||||
|
||||
private void OnEquippedGearset(string name, int id, int prior, byte _, byte job)
|
||||
{
|
||||
equipFlags &= ~totalEquipFlags;
|
||||
if (equipFlags == 0)
|
||||
if (!_config.EnableAutoDesigns)
|
||||
return;
|
||||
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
var flag = slot.ToFlag();
|
||||
if (equipFlags.HasFlag(flag))
|
||||
{
|
||||
var item = design.Item(slot);
|
||||
if (!_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _))
|
||||
{
|
||||
if (!respectManual || state[slot, false] is not StateChanged.Source.Manual)
|
||||
_state.ChangeItem(state, slot, item, source);
|
||||
totalEquipFlags |= flag;
|
||||
}
|
||||
}
|
||||
var (player, data) = _objects.PlayerData;
|
||||
if (!player.IsValid)
|
||||
return;
|
||||
|
||||
var stainFlag = slot.ToStainFlag();
|
||||
if (equipFlags.HasFlag(stainFlag))
|
||||
{
|
||||
if (!respectManual || state[slot, true] is not StateChanged.Source.Manual)
|
||||
_state.ChangeStain(state, slot, design.Stain(slot), source);
|
||||
totalEquipFlags |= stainFlag;
|
||||
}
|
||||
}
|
||||
if (!GetPlayerSet(player, out var set) || !_state.TryGetValue(player, out var state))
|
||||
return;
|
||||
|
||||
if (equipFlags.HasFlag(EquipFlag.Mainhand))
|
||||
{
|
||||
var item = design.Item(EquipSlot.MainHand);
|
||||
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
|
||||
var checkState = !respectManual || state[EquipSlot.MainHand, false] is not StateChanged.Source.Manual;
|
||||
if (checkUnlock && checkState)
|
||||
{
|
||||
if (fromJobChange)
|
||||
{
|
||||
_jobChangeMainhand.TryAdd(item.Type, (item, source));
|
||||
_jobChangeState = state;
|
||||
}
|
||||
else if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type)
|
||||
{
|
||||
_state.ChangeItem(state, EquipSlot.MainHand, item, source);
|
||||
totalEquipFlags |= EquipFlag.Mainhand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (equipFlags.HasFlag(EquipFlag.Offhand))
|
||||
{
|
||||
var item = design.Item(EquipSlot.OffHand);
|
||||
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
|
||||
var checkState = !respectManual || state[EquipSlot.OffHand, false] is not StateChanged.Source.Manual;
|
||||
if (checkUnlock && checkState)
|
||||
{
|
||||
if (fromJobChange)
|
||||
{
|
||||
_jobChangeOffhand.TryAdd(item.Type, (item, source));
|
||||
_jobChangeState = state;
|
||||
}
|
||||
else if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type)
|
||||
{
|
||||
_state.ChangeItem(state, EquipSlot.OffHand, item, source);
|
||||
totalEquipFlags |= EquipFlag.Offhand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (equipFlags.HasFlag(EquipFlag.MainhandStain))
|
||||
{
|
||||
if (!respectManual || state[EquipSlot.MainHand, true] is not StateChanged.Source.Manual)
|
||||
_state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), source);
|
||||
totalEquipFlags |= EquipFlag.MainhandStain;
|
||||
}
|
||||
|
||||
if (equipFlags.HasFlag(EquipFlag.OffhandStain))
|
||||
{
|
||||
if (!respectManual || state[EquipSlot.OffHand, true] is not StateChanged.Source.Manual)
|
||||
_state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), source);
|
||||
totalEquipFlags |= EquipFlag.OffhandStain;
|
||||
}
|
||||
var respectManual = prior == id;
|
||||
NewGearsetId = id;
|
||||
Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, prior == id, out var forcedRedraw);
|
||||
NewGearsetId = -1;
|
||||
foreach (var actor in data.Objects)
|
||||
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
|
||||
}
|
||||
|
||||
private void ReduceCustomize(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags,
|
||||
bool respectManual, StateChanged.Source source)
|
||||
public static unsafe bool CheckGearset(short check)
|
||||
{
|
||||
customizeFlags &= ~totalCustomizeFlags;
|
||||
if (customizeFlags == 0)
|
||||
return;
|
||||
if (NewGearsetId != -1)
|
||||
return check == NewGearsetId;
|
||||
|
||||
var customize = state.ModelData.Customize;
|
||||
CustomizeFlag fixFlags = 0;
|
||||
var module = RaptureGearsetModule.Instance();
|
||||
if (module == null)
|
||||
return false;
|
||||
|
||||
// Skip anything not human.
|
||||
if (!state.ModelData.IsHuman || !design.IsHuman)
|
||||
return;
|
||||
|
||||
if (customizeFlags.HasFlag(CustomizeFlag.Clan))
|
||||
{
|
||||
if (!respectManual || state[CustomizeIndex.Clan] is not StateChanged.Source.Manual)
|
||||
fixFlags |= _customizations.ChangeClan(ref customize, design.Customize.Clan);
|
||||
customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race);
|
||||
totalCustomizeFlags |= CustomizeFlag.Clan | CustomizeFlag.Race;
|
||||
}
|
||||
|
||||
if (customizeFlags.HasFlag(CustomizeFlag.Gender))
|
||||
{
|
||||
if (!respectManual || state[CustomizeIndex.Gender] is not StateChanged.Source.Manual)
|
||||
fixFlags |= _customizations.ChangeGender(ref customize, design.Customize.Gender);
|
||||
customizeFlags &= ~CustomizeFlag.Gender;
|
||||
totalCustomizeFlags |= CustomizeFlag.Gender;
|
||||
}
|
||||
|
||||
if (fixFlags != 0)
|
||||
_state.ChangeCustomize(state, customize, fixFlags, source);
|
||||
|
||||
if (customizeFlags.HasFlag(CustomizeFlag.Face))
|
||||
{
|
||||
if (!respectManual || state[CustomizeIndex.Face] is not StateChanged.Source.Manual)
|
||||
_state.ChangeCustomize(state, CustomizeIndex.Face, design.Customize.Face, source);
|
||||
customizeFlags &= ~CustomizeFlag.Face;
|
||||
totalCustomizeFlags |= CustomizeFlag.Face;
|
||||
}
|
||||
|
||||
var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
|
||||
var face = state.ModelData.Customize.Face;
|
||||
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
var flag = index.ToFlag();
|
||||
if (!customizeFlags.HasFlag(flag))
|
||||
continue;
|
||||
|
||||
var value = design.Customize[index];
|
||||
if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data))
|
||||
{
|
||||
if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _))
|
||||
continue;
|
||||
|
||||
if (!respectManual || state[index] is not StateChanged.Source.Manual)
|
||||
_state.ChangeCustomize(state, index, value, source);
|
||||
totalCustomizeFlags |= flag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReduceMeta(ActorState state, in DesignData design, bool applyHat, bool applyVisor, bool applyWeapon, bool applyWet,
|
||||
ref byte totalMetaFlags, bool respectManual, StateChanged.Source source)
|
||||
{
|
||||
if (applyHat && (totalMetaFlags & 0x01) == 0)
|
||||
{
|
||||
if (!respectManual || state[ActorState.MetaIndex.HatState] is not StateChanged.Source.Manual)
|
||||
_state.ChangeHatState(state, design.IsHatVisible(), source);
|
||||
totalMetaFlags |= 0x01;
|
||||
}
|
||||
|
||||
if (applyVisor && (totalMetaFlags & 0x02) == 0)
|
||||
{
|
||||
if (!respectManual || state[ActorState.MetaIndex.VisorState] is not StateChanged.Source.Manual)
|
||||
_state.ChangeVisorState(state, design.IsVisorToggled(), source);
|
||||
totalMetaFlags |= 0x02;
|
||||
}
|
||||
|
||||
if (applyWeapon && (totalMetaFlags & 0x04) == 0)
|
||||
{
|
||||
if (!respectManual || state[ActorState.MetaIndex.WeaponState] is not StateChanged.Source.Manual)
|
||||
_state.ChangeWeaponState(state, design.IsWeaponVisible(), source);
|
||||
totalMetaFlags |= 0x04;
|
||||
}
|
||||
|
||||
if (applyWet && (totalMetaFlags & 0x08) == 0)
|
||||
{
|
||||
if (!respectManual || state[ActorState.MetaIndex.Wetness] is not StateChanged.Source.Manual)
|
||||
_state.ChangeWetness(state, design.IsWet(), source);
|
||||
totalMetaFlags |= 0x08;
|
||||
}
|
||||
return check == module->CurrentGearsetIndex;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Designs.History;
|
||||
using Glamourer.Designs.Special;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Structs;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Automation;
|
||||
|
||||
|
|
@ -26,27 +24,32 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
private readonly JobService _jobs;
|
||||
private readonly DesignManager _designs;
|
||||
private readonly ActorService _actors;
|
||||
private readonly AutomationChanged _event;
|
||||
private readonly DesignChanged _designEvent;
|
||||
private readonly JobService _jobs;
|
||||
private readonly DesignManager _designs;
|
||||
private readonly ActorManager _actors;
|
||||
private readonly AutomationChanged _event;
|
||||
private readonly DesignChanged _designEvent;
|
||||
private readonly RandomDesignGenerator _randomDesigns;
|
||||
private readonly QuickSelectedDesign _quickSelectedDesign;
|
||||
|
||||
private readonly List<AutoDesignSet> _data = new();
|
||||
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = new();
|
||||
private readonly List<AutoDesignSet> _data = [];
|
||||
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = [];
|
||||
|
||||
public IReadOnlyDictionary<ActorIdentifier, AutoDesignSet> EnabledSets
|
||||
=> _enabled;
|
||||
|
||||
public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
|
||||
FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent)
|
||||
public AutoDesignManager(JobService jobs, ActorManager actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
|
||||
FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent, RandomDesignGenerator randomDesigns,
|
||||
QuickSelectedDesign quickSelectedDesign)
|
||||
{
|
||||
_jobs = jobs;
|
||||
_actors = actors;
|
||||
_saveService = saveService;
|
||||
_designs = designs;
|
||||
_event = @event;
|
||||
_designEvent = designEvent;
|
||||
_jobs = jobs;
|
||||
_actors = actors;
|
||||
_saveService = saveService;
|
||||
_designs = designs;
|
||||
_event = @event;
|
||||
_designEvent = designEvent;
|
||||
_randomDesigns = randomDesigns;
|
||||
_quickSelectedDesign = quickSelectedDesign;
|
||||
_designEvent.Subscribe(OnDesignChange, DesignChanged.Priority.AutoDesignManager);
|
||||
Load();
|
||||
migrator.ConsumeMigratedData(_actors, fileSystem, this);
|
||||
|
|
@ -232,18 +235,34 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
_event.Invoke(AutomationChanged.Type.ChangedBase, set, (old, newBase));
|
||||
}
|
||||
|
||||
public void AddDesign(AutoDesignSet set, Design? design)
|
||||
public void ChangeResetSettings(int whichSet, bool newValue)
|
||||
{
|
||||
if (whichSet >= _data.Count || whichSet < 0)
|
||||
return;
|
||||
|
||||
var set = _data[whichSet];
|
||||
if (newValue == set.ResetTemporarySettings)
|
||||
return;
|
||||
|
||||
var old = set.ResetTemporarySettings;
|
||||
set.ResetTemporarySettings = newValue;
|
||||
Save();
|
||||
Glamourer.Log.Debug($"Changed resetting of temporary settings of set {whichSet + 1} from {old} to {newValue}.");
|
||||
_event.Invoke(AutomationChanged.Type.ChangedTemporarySettingsReset, set, newValue);
|
||||
}
|
||||
|
||||
public void AddDesign(AutoDesignSet set, IDesignStandIn design)
|
||||
{
|
||||
var newDesign = new AutoDesign()
|
||||
{
|
||||
Design = design,
|
||||
ApplicationType = AutoDesign.Type.All,
|
||||
Jobs = _jobs.JobGroups[1],
|
||||
Design = design,
|
||||
Type = ApplicationType.All,
|
||||
Jobs = _jobs.JobGroups[1],
|
||||
};
|
||||
set.Designs.Add(newDesign);
|
||||
Save();
|
||||
Glamourer.Log.Debug(
|
||||
$"Added new associated design {design?.Identifier.ToString() ?? "Reverter"} as design {set.Designs.Count} to design set.");
|
||||
$"Added new associated design {design.ResolveName(true)} as design {set.Designs.Count} to design set.");
|
||||
_event.Invoke(AutomationChanged.Type.AddedDesign, set, set.Designs.Count - 1);
|
||||
}
|
||||
|
||||
|
|
@ -283,20 +302,20 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
_event.Invoke(AutomationChanged.Type.MovedDesign, set, (from, to));
|
||||
}
|
||||
|
||||
public void ChangeDesign(AutoDesignSet set, int which, Design? newDesign)
|
||||
public void ChangeDesign(AutoDesignSet set, int which, IDesignStandIn newDesign)
|
||||
{
|
||||
if (which >= set.Designs.Count || which < 0)
|
||||
return;
|
||||
|
||||
var design = set.Designs[which];
|
||||
if (design.Design?.Identifier == newDesign?.Identifier)
|
||||
if (design.Design.Equals(newDesign))
|
||||
return;
|
||||
|
||||
var old = design.Design;
|
||||
design.Design = newDesign;
|
||||
Save();
|
||||
Glamourer.Log.Debug(
|
||||
$"Changed linked design from {old?.Identifier.ToString() ?? "Reverter"} to {newDesign?.Identifier.ToString() ?? "Reverter"} for associated design {which + 1} in design set.");
|
||||
$"Changed linked design from {old.ResolveName(true)} to {newDesign.ResolveName(true)} for associated design {which + 1} in design set.");
|
||||
_event.Invoke(AutomationChanged.Type.ChangedDesign, set, (which, old, newDesign));
|
||||
}
|
||||
|
||||
|
|
@ -306,6 +325,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
return;
|
||||
|
||||
var design = set.Designs[which];
|
||||
|
||||
if (design.Jobs.Id == jobs.Id)
|
||||
return;
|
||||
|
||||
|
|
@ -316,21 +336,51 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs));
|
||||
}
|
||||
|
||||
public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type)
|
||||
public void ChangeGearsetCondition(AutoDesignSet set, int which, short index)
|
||||
{
|
||||
if (which >= set.Designs.Count || which < 0)
|
||||
return;
|
||||
|
||||
type &= AutoDesign.Type.All;
|
||||
var design = set.Designs[which];
|
||||
if (design.ApplicationType == type)
|
||||
if (design.GearsetIndex == index)
|
||||
return;
|
||||
|
||||
var old = design.ApplicationType;
|
||||
design.ApplicationType = type;
|
||||
var old = design.GearsetIndex;
|
||||
design.GearsetIndex = index;
|
||||
Save();
|
||||
Glamourer.Log.Debug($"Changed application type from {old} to {type} for associated design {which + 1} in design set.");
|
||||
_event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, type));
|
||||
Glamourer.Log.Debug($"Changed gearset condition from {old} to {index} for associated design {which + 1} in design set.");
|
||||
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, index));
|
||||
}
|
||||
|
||||
public void ChangeApplicationType(AutoDesignSet set, int which, ApplicationType applicationType)
|
||||
{
|
||||
if (which >= set.Designs.Count || which < 0)
|
||||
return;
|
||||
|
||||
applicationType &= ApplicationType.All;
|
||||
var design = set.Designs[which];
|
||||
if (design.Type == applicationType)
|
||||
return;
|
||||
|
||||
var old = design.Type;
|
||||
design.Type = applicationType;
|
||||
Save();
|
||||
Glamourer.Log.Debug($"Changed application type from {old} to {applicationType} for associated design {which + 1} in design set.");
|
||||
_event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, applicationType));
|
||||
}
|
||||
|
||||
public void ChangeData(AutoDesignSet set, int which, object data)
|
||||
{
|
||||
if (which >= set.Designs.Count || which < 0)
|
||||
return;
|
||||
|
||||
var design = set.Designs[which];
|
||||
if (!design.Design.ChangeData(data))
|
||||
return;
|
||||
|
||||
Save();
|
||||
Glamourer.Log.Debug($"Changed additional design data for associated design {which + 1} in design set.");
|
||||
_event.Invoke(AutomationChanged.Type.ChangedData, set, (which, data));
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
|
|
@ -338,10 +388,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var j = new JsonTextWriter(writer)
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
using var j = new JsonTextWriter(writer);
|
||||
j.Formatting = Formatting.Indented;
|
||||
Serialize().WriteTo(j);
|
||||
}
|
||||
|
||||
|
|
@ -404,7 +452,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
continue;
|
||||
}
|
||||
|
||||
var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject);
|
||||
var id = _actors.FromJson(obj["Identifier"] as JObject);
|
||||
if (!IdentifierValid(id, out var group))
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", NotificationType.Warning);
|
||||
|
|
@ -413,8 +461,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
|
||||
var set = new AutoDesignSet(name, group)
|
||||
{
|
||||
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
|
||||
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current,
|
||||
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
|
||||
ResetTemporarySettings = obj["ResetTemporarySettings"]?.ToObject<bool>() ?? false,
|
||||
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current,
|
||||
};
|
||||
|
||||
if (set.Enabled)
|
||||
|
|
@ -440,8 +489,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
continue;
|
||||
}
|
||||
|
||||
var design = ToDesignObject(set.Name, j);
|
||||
if (design != null)
|
||||
if (ToDesignObject(set.Name, j) is { } design)
|
||||
set.Designs.Add(design);
|
||||
}
|
||||
}
|
||||
|
|
@ -449,58 +497,85 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
|
||||
private AutoDesign? ToDesignObject(string setName, JObject jObj)
|
||||
{
|
||||
var designIdentifier = jObj["Design"]?.ToObject<string?>();
|
||||
Design? design = null;
|
||||
// designIdentifier == null means Revert-Design.
|
||||
if (designIdentifier != null)
|
||||
var designIdentifier = jObj["Design"]?.ToObject<string?>();
|
||||
IDesignStandIn? design;
|
||||
// designIdentifier == null means Revert-Design for backwards compatibility
|
||||
if (designIdentifier is null or RevertDesign.SerializedName)
|
||||
{
|
||||
design = new RevertDesign();
|
||||
}
|
||||
else if (designIdentifier is RandomDesign.SerializedName)
|
||||
{
|
||||
design = new RandomDesign(_randomDesigns);
|
||||
}
|
||||
else if (designIdentifier is QuickSelectedDesign.SerializedName)
|
||||
{
|
||||
design = _quickSelectedDesign;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (designIdentifier.Length == 0)
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", NotificationType.Warning);
|
||||
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.",
|
||||
NotificationType.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(designIdentifier, out var guid))
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", NotificationType.Warning);
|
||||
Glamourer.Messager.NotificationMessage(
|
||||
$"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.",
|
||||
NotificationType.Warning);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
design = _designs.Designs.FirstOrDefault(d => d.Identifier == guid);
|
||||
if (design == null)
|
||||
if (!_designs.Designs.TryGetValue(guid, out var d))
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage(
|
||||
$"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", NotificationType.Warning);
|
||||
$"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.",
|
||||
NotificationType.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
design = d;
|
||||
}
|
||||
|
||||
var applicationType = (AutoDesign.Type)(jObj["ApplicationType"]?.ToObject<uint>() ?? 0);
|
||||
design.ParseData(jObj);
|
||||
|
||||
var ret = new AutoDesign()
|
||||
// ApplicationType is a migration from an older property name.
|
||||
var applicationType = (ApplicationType)(jObj["Type"]?.ToObject<uint>() ?? jObj["ApplicationType"]?.ToObject<uint>() ?? 0);
|
||||
|
||||
var ret = new AutoDesign
|
||||
{
|
||||
Design = design,
|
||||
ApplicationType = applicationType & AutoDesign.Type.All,
|
||||
Design = design,
|
||||
Type = applicationType & ApplicationType.All,
|
||||
};
|
||||
return ParseConditions(setName, jObj, ret) ? ret : null;
|
||||
}
|
||||
|
||||
private bool ParseConditions(string setName, JObject jObj, AutoDesign ret)
|
||||
{
|
||||
var conditions = jObj["Conditions"];
|
||||
if (conditions == null)
|
||||
return ret;
|
||||
return true;
|
||||
|
||||
var jobs = conditions["JobGroup"]?.ToObject<int>() ?? -1;
|
||||
if (jobs >= 0)
|
||||
{
|
||||
if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup))
|
||||
if (!_jobs.JobGroups.TryGetValue((JobGroupId)jobs, out var jobGroup))
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", NotificationType.Warning);
|
||||
return null;
|
||||
Glamourer.Messager.NotificationMessage(
|
||||
$"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.",
|
||||
NotificationType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
ret.Jobs = jobGroup;
|
||||
}
|
||||
|
||||
return ret;
|
||||
ret.GearsetIndex = conditions["Gearset"]?.ToObject<short>() ?? -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
|
|
@ -513,12 +588,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
IdentifierType.Player => true,
|
||||
IdentifierType.Retainer => true,
|
||||
IdentifierType.Npc => true,
|
||||
IdentifierType.Owned => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (!validType)
|
||||
{
|
||||
group = Array.Empty<ActorIdentifier>();
|
||||
group = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -529,42 +605,42 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
|||
private ActorIdentifier[] GetGroup(ActorIdentifier identifier)
|
||||
{
|
||||
if (!identifier.IsValid)
|
||||
return Array.Empty<ActorIdentifier>();
|
||||
return [];
|
||||
|
||||
return identifier.Type switch
|
||||
{
|
||||
IdentifierType.Player =>
|
||||
[
|
||||
identifier.CreatePermanent(),
|
||||
],
|
||||
IdentifierType.Retainer =>
|
||||
[
|
||||
_actors.CreateRetainer(identifier.PlayerName,
|
||||
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
|
||||
? ActorIdentifier.RetainerType.Mannequin
|
||||
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
|
||||
],
|
||||
IdentifierType.Npc => CreateNpcs(_actors, identifier),
|
||||
IdentifierType.Owned => CreateNpcs(_actors, identifier),
|
||||
_ => [],
|
||||
};
|
||||
|
||||
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
|
||||
{
|
||||
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
|
||||
var table = identifier.Kind switch
|
||||
{
|
||||
ObjectKind.BattleNpc => manager.Data.BNpcs,
|
||||
ObjectKind.BattleNpc => (IReadOnlyDictionary<NpcId, string>)manager.Data.BNpcs,
|
||||
ObjectKind.EventNpc => manager.Data.ENpcs,
|
||||
_ => new Dictionary<uint, string>(),
|
||||
_ => new Dictionary<NpcId, string>(),
|
||||
};
|
||||
return table.Where(kvp => kvp.Value == name)
|
||||
.Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id,
|
||||
identifier.Kind,
|
||||
kvp.Key)).ToArray();
|
||||
identifier.Kind, kvp.Key)).ToArray();
|
||||
}
|
||||
|
||||
return identifier.Type switch
|
||||
{
|
||||
IdentifierType.Player => new[]
|
||||
{
|
||||
identifier.CreatePermanent(),
|
||||
},
|
||||
IdentifierType.Retainer => new[]
|
||||
{
|
||||
_actors.AwaitedService.CreateRetainer(identifier.PlayerName,
|
||||
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
|
||||
? ActorIdentifier.RetainerType.Mannequin
|
||||
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
|
||||
},
|
||||
IdentifierType.Npc => CreateNpcs(_actors.AwaitedService, identifier),
|
||||
_ => Array.Empty<ActorIdentifier>(),
|
||||
};
|
||||
}
|
||||
|
||||
private void OnDesignChange(DesignChanged.Type type, Design design, object? data)
|
||||
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _)
|
||||
{
|
||||
if (type is not DesignChanged.Type.Deleted)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace Glamourer.Automation;
|
||||
|
||||
public class AutoDesignSet
|
||||
public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List<AutoDesign> designs)
|
||||
{
|
||||
public readonly List<AutoDesign> Designs;
|
||||
public readonly List<AutoDesign> Designs = designs;
|
||||
|
||||
public string Name;
|
||||
public ActorIdentifier[] Identifiers;
|
||||
public string Name = name;
|
||||
public ActorIdentifier[] Identifiers = identifiers;
|
||||
public bool Enabled;
|
||||
public Base BaseState = Base.Current;
|
||||
public Base BaseState = Base.Current;
|
||||
public bool ResetTemporarySettings = false;
|
||||
|
||||
public JObject Serialize()
|
||||
{
|
||||
|
|
@ -21,25 +21,19 @@ public class AutoDesignSet
|
|||
|
||||
return new JObject()
|
||||
{
|
||||
["Name"] = Name,
|
||||
["Identifier"] = Identifiers[0].ToJson(),
|
||||
["Enabled"] = Enabled,
|
||||
["BaseState"] = BaseState.ToString(),
|
||||
["Designs"] = list,
|
||||
["Name"] = Name,
|
||||
["Identifier"] = Identifiers[0].ToJson(),
|
||||
["Enabled"] = Enabled,
|
||||
["BaseState"] = BaseState.ToString(),
|
||||
["ResetTemporarySettings"] = ResetTemporarySettings.ToString(),
|
||||
["Designs"] = list,
|
||||
};
|
||||
}
|
||||
|
||||
public AutoDesignSet(string name, params ActorIdentifier[] identifiers)
|
||||
: this(name, identifiers, new List<AutoDesign>())
|
||||
: this(name, identifiers, [])
|
||||
{ }
|
||||
|
||||
public AutoDesignSet(string name, ActorIdentifier[] identifiers, List<AutoDesign> designs)
|
||||
{
|
||||
Name = name;
|
||||
Identifiers = identifiers;
|
||||
Designs = designs;
|
||||
}
|
||||
|
||||
public enum Base : byte
|
||||
{
|
||||
Current,
|
||||
|
|
|
|||
|
|
@ -1,61 +1,51 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Structs;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Glamourer.Automation;
|
||||
|
||||
public class FixedDesignMigrator
|
||||
public class FixedDesignMigrator(JobService jobs)
|
||||
{
|
||||
private readonly JobService _jobs;
|
||||
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
|
||||
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
|
||||
|
||||
public FixedDesignMigrator(JobService jobs)
|
||||
=> _jobs = jobs;
|
||||
|
||||
public void ConsumeMigratedData(ActorService actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
|
||||
public void ConsumeMigratedData(ActorManager actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
|
||||
{
|
||||
if (_migratedData == null)
|
||||
return;
|
||||
|
||||
foreach (var data in _migratedData)
|
||||
foreach (var (name, data) in _migratedData)
|
||||
{
|
||||
var allEnabled = true;
|
||||
var name = data.Name;
|
||||
if (autoManager.Any(d => name == d.Name))
|
||||
continue;
|
||||
|
||||
var id = ActorIdentifier.Invalid;
|
||||
if (ByteString.FromString(data.Name, out var byteString, false))
|
||||
if (ByteString.FromString(name, out var byteString))
|
||||
{
|
||||
id = actors.AwaitedService.CreatePlayer(byteString, ushort.MaxValue);
|
||||
id = actors.CreatePlayer(byteString, ushort.MaxValue);
|
||||
if (!id.IsValid)
|
||||
id = actors.AwaitedService.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both);
|
||||
id = actors.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both);
|
||||
}
|
||||
|
||||
if (!id.IsValid)
|
||||
{
|
||||
byteString = ByteString.FromSpanUnsafe("Mig Ration"u8, true, false, true);
|
||||
id = actors.AwaitedService.CreatePlayer(byteString, actors.AwaitedService.Data.Worlds.First().Key);
|
||||
id = actors.CreatePlayer(byteString, actors.Data.Worlds.First().Key);
|
||||
if (!id.IsValid)
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {data.Name}.", NotificationType.Error);
|
||||
allEnabled = false;
|
||||
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {name}.", NotificationType.Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
autoManager.AddDesignSet(name, id);
|
||||
autoManager.SetState(autoManager.Count - 1, allEnabled);
|
||||
autoManager.SetState(autoManager.Count - 1, true);
|
||||
var set = autoManager[^1];
|
||||
foreach (var design in data.Data.AsEnumerable().Reverse())
|
||||
foreach (var design in data.AsEnumerable().Reverse())
|
||||
{
|
||||
if (!designFileSystem.Find(design.Item1, out var child) || child is not DesignFileSystem.Leaf leaf)
|
||||
{
|
||||
|
|
@ -66,7 +56,7 @@ public class FixedDesignMigrator
|
|||
|
||||
autoManager.AddDesign(set, leaf.Value);
|
||||
autoManager.ChangeJobCondition(set, set.Designs.Count - 1, design.Item2);
|
||||
autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? AutoDesign.Type.All : 0);
|
||||
autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? ApplicationType.All : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +86,7 @@ public class FixedDesignMigrator
|
|||
}
|
||||
|
||||
var job = obj["JobGroups"]?.ToObject<int>() ?? -1;
|
||||
if (job < 0 || !_jobs.JobGroups.TryGetValue((ushort)job, out var group))
|
||||
if (job < 0 || !jobs.JobGroups.TryGetValue((JobGroupId)job, out var group))
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage("Could not semi-migrate fixed design: Invalid job group specified.",
|
||||
NotificationType.Warning);
|
||||
|
|
|
|||
|
|
@ -1,51 +1,97 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Gui;
|
||||
using Glamourer.Gui.Tabs.DesignTab;
|
||||
using Glamourer.Services;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Widgets;
|
||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||
|
||||
namespace Glamourer;
|
||||
|
||||
public enum HeightDisplayType
|
||||
{
|
||||
None,
|
||||
Centimetre,
|
||||
Metre,
|
||||
Wrong,
|
||||
WrongFoot,
|
||||
Corgi,
|
||||
OlympicPool,
|
||||
}
|
||||
|
||||
public class DefaultDesignSettings
|
||||
{
|
||||
public bool AlwaysForceRedrawing = false;
|
||||
public bool ResetAdvancedDyes = false;
|
||||
public bool ShowQuickDesignBar = true;
|
||||
public bool ResetTemporarySettings = false;
|
||||
public bool Locked = false;
|
||||
}
|
||||
|
||||
public class Configuration : IPluginConfiguration, ISavable
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public bool UseRestrictedGearProtection { get; set; } = false;
|
||||
public bool OpenFoldersByDefault { get; set; } = false;
|
||||
public bool AutoRedrawEquipOnChanges { get; set; } = false;
|
||||
public bool EnableAutoDesigns { get; set; } = true;
|
||||
public bool IncognitoMode { get; set; } = false;
|
||||
public bool UnlockDetailMode { get; set; } = true;
|
||||
public bool HideApplyCheckmarks { get; set; } = false;
|
||||
public bool SmallEquip { get; set; } = false;
|
||||
public bool UnlockedItemMode { get; set; } = false;
|
||||
public byte DisableFestivals { get; set; } = 1;
|
||||
public bool EnableGameContextMenu { get; set; } = true;
|
||||
public bool HideWindowInCutscene { get; set; } = false;
|
||||
public bool ShowAutomationSetEditing { get; set; } = true;
|
||||
public bool ShowAllAutomatedApplicationRules { get; set; } = true;
|
||||
public bool ShowUnlockedItemWarnings { get; set; } = true;
|
||||
public bool RevertManualChangesOnZoneChange { get; set; } = false;
|
||||
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
|
||||
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
|
||||
[JsonIgnore]
|
||||
public readonly EphemeralConfig Ephemeral;
|
||||
|
||||
public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion;
|
||||
public bool AttachToPcp { get; set; } = true;
|
||||
public bool UseRestrictedGearProtection { get; set; } = false;
|
||||
public bool OpenFoldersByDefault { get; set; } = false;
|
||||
public bool AutoRedrawEquipOnChanges { get; set; } = false;
|
||||
public bool EnableAutoDesigns { get; set; } = true;
|
||||
public bool HideApplyCheckmarks { get; set; } = false;
|
||||
public bool SmallEquip { get; set; } = false;
|
||||
public bool UnlockedItemMode { get; set; } = false;
|
||||
public byte DisableFestivals { get; set; } = 1;
|
||||
public bool EnableGameContextMenu { get; set; } = true;
|
||||
public bool HideWindowInCutscene { get; set; } = false;
|
||||
public bool ShowAutomationSetEditing { get; set; } = true;
|
||||
public bool ShowAllAutomatedApplicationRules { get; set; } = true;
|
||||
public bool ShowUnlockedItemWarnings { get; set; } = true;
|
||||
public bool RevertManualChangesOnZoneChange { get; set; } = false;
|
||||
public bool ShowQuickBarInTabs { get; set; } = true;
|
||||
public bool OpenWindowAtStart { get; set; } = false;
|
||||
public bool ShowWindowWhenUiHidden { get; set; } = false;
|
||||
public bool KeepAdvancedDyesAttached { get; set; } = true;
|
||||
public bool ShowPalettePlusImport { get; set; } = true;
|
||||
public bool UseFloatForColors { get; set; } = true;
|
||||
public bool UseRgbForColors { get; set; } = true;
|
||||
public bool ShowColorConfig { get; set; } = true;
|
||||
public bool ChangeEntireItem { get; set; } = false;
|
||||
public bool AlwaysApplyAssociatedMods { get; set; } = true;
|
||||
public bool UseTemporarySettings { get; set; } = true;
|
||||
public bool AllowDoubleClickToApply { get; set; } = false;
|
||||
public bool RespectManualOnAutomationUpdate { get; set; } = false;
|
||||
public bool PreventRandomRepeats { get; set; } = false;
|
||||
public string PcpFolder { get; set; } = "PCP";
|
||||
public string PcpColor { get; set; } = "";
|
||||
|
||||
public DesignPanelFlag HideDesignPanel { get; set; } = 0;
|
||||
public DesignPanelFlag AutoExpandDesignPanel { get; set; } = 0;
|
||||
|
||||
public DefaultDesignSettings DefaultDesignSettings { get; set; } = new();
|
||||
|
||||
public HeightDisplayType HeightDisplayType { get; set; } = HeightDisplayType.Centimetre;
|
||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||
public ModifiableHotkey ToggleQuickDesignBar { get; set; } = new(VirtualKey.NO_KEY);
|
||||
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
|
||||
public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control);
|
||||
public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New;
|
||||
|
||||
public QdbButtons QdbButtons { get; set; } =
|
||||
QdbButtons.ApplyDesign | QdbButtons.RevertAll | QdbButtons.RevertAutomation | QdbButtons.RevertAdvancedDyes;
|
||||
|
||||
[JsonConverter(typeof(SortModeConverter))]
|
||||
[JsonProperty(Order = int.MaxValue)]
|
||||
public ISortMode<Design> SortMode { get; set; } = ISortMode<Design>.FoldersFirst;
|
||||
|
||||
public List<(string Code, bool Enabled)> Codes { get; set; } = new();
|
||||
public List<(string Code, bool Enabled)> Codes { get; set; } = [];
|
||||
|
||||
#if DEBUG
|
||||
public bool DebugMode { get; set; } = true;
|
||||
|
|
@ -61,24 +107,18 @@ public class Configuration : IPluginConfiguration, ISavable
|
|||
[JsonIgnore]
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
public Configuration(SaveService saveService, ConfigMigrationService migrator)
|
||||
public Configuration(SaveService saveService, ConfigMigrationService migrator, EphemeralConfig ephemeral)
|
||||
{
|
||||
_saveService = saveService;
|
||||
Ephemeral = ephemeral;
|
||||
Load(migrator);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
=> _saveService.DelaySave(this);
|
||||
|
||||
public void Load(ConfigMigrationService migrator)
|
||||
private void Load(ConfigMigrationService migrator)
|
||||
{
|
||||
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
|
||||
{
|
||||
Glamourer.Log.Error(
|
||||
$"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
|
||||
errorArgs.ErrorContext.Handled = true;
|
||||
}
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.ConfigFile))
|
||||
return;
|
||||
|
||||
|
|
@ -99,6 +139,14 @@ public class Configuration : IPluginConfiguration, ISavable
|
|||
}
|
||||
|
||||
migrator.Migrate(this);
|
||||
return;
|
||||
|
||||
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
|
||||
{
|
||||
Glamourer.Log.Error(
|
||||
$"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
|
||||
errorArgs.ErrorContext.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
|
|
@ -106,17 +154,18 @@ public class Configuration : IPluginConfiguration, ISavable
|
|||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
using var jWriter = new JsonTextWriter(writer);
|
||||
jWriter.Formatting = Formatting.Indented;
|
||||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(jWriter, this);
|
||||
}
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const int CurrentVersion = 4;
|
||||
public const int CurrentVersion = 8;
|
||||
|
||||
public static readonly ISortMode<Design>[] ValidSortModes =
|
||||
{
|
||||
[
|
||||
ISortMode<Design>.FoldersFirst,
|
||||
ISortMode<Design>.Lexicographical,
|
||||
new DesignFileSystem.CreationDate(),
|
||||
|
|
@ -129,7 +178,7 @@ public class Configuration : IPluginConfiguration, ISavable
|
|||
ISortMode<Design>.InverseFoldersLast,
|
||||
ISortMode<Design>.InternalOrder,
|
||||
ISortMode<Design>.InverseInternalOrder,
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary> Convert SortMode Types to their name. </summary>
|
||||
|
|
|
|||
96
Glamourer/DesignPanelFlag.cs
Normal file
96
Glamourer/DesignPanelFlag.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using Glamourer.Designs;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Text.EndObjects;
|
||||
|
||||
namespace Glamourer;
|
||||
|
||||
[Flags]
|
||||
public enum DesignPanelFlag : uint
|
||||
{
|
||||
Customization = 0x0001,
|
||||
Equipment = 0x0002,
|
||||
AdvancedCustomizations = 0x0004,
|
||||
AdvancedDyes = 0x0008,
|
||||
AppearanceDetails = 0x0010,
|
||||
DesignDetails = 0x0020,
|
||||
ModAssociations = 0x0040,
|
||||
DesignLinks = 0x0080,
|
||||
ApplicationRules = 0x0100,
|
||||
DebugData = 0x0200,
|
||||
}
|
||||
|
||||
public static class DesignPanelFlagExtensions
|
||||
{
|
||||
public static ReadOnlySpan<byte> ToName(this DesignPanelFlag flag)
|
||||
=> flag switch
|
||||
{
|
||||
DesignPanelFlag.Customization => "Customization"u8,
|
||||
DesignPanelFlag.Equipment => "Equipment"u8,
|
||||
DesignPanelFlag.AdvancedCustomizations => "Advanced Customization"u8,
|
||||
DesignPanelFlag.AdvancedDyes => "Advanced Dyes"u8,
|
||||
DesignPanelFlag.DesignDetails => "Design Details"u8,
|
||||
DesignPanelFlag.ApplicationRules => "Application Rules"u8,
|
||||
DesignPanelFlag.ModAssociations => "Mod Associations"u8,
|
||||
DesignPanelFlag.DesignLinks => "Design Links"u8,
|
||||
DesignPanelFlag.DebugData => "Debug Data"u8,
|
||||
DesignPanelFlag.AppearanceDetails => "Appearance Details"u8,
|
||||
_ => ""u8,
|
||||
};
|
||||
|
||||
public static CollapsingHeader Header(this DesignPanelFlag flag, Configuration config)
|
||||
{
|
||||
if (config.HideDesignPanel.HasFlag(flag))
|
||||
return new CollapsingHeader()
|
||||
{
|
||||
Disposed = true,
|
||||
};
|
||||
|
||||
var expand = config.AutoExpandDesignPanel.HasFlag(flag);
|
||||
return ImUtf8.CollapsingHeaderId(flag.ToName(), expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
|
||||
}
|
||||
|
||||
public static void DrawTable(ReadOnlySpan<byte> label, DesignPanelFlag hidden, DesignPanelFlag expanded, Action<DesignPanelFlag> setterHide,
|
||||
Action<DesignPanelFlag> setterExpand)
|
||||
{
|
||||
var checkBoxWidth = Math.Max(ImGui.GetFrameHeight(), ImUtf8.CalcTextSize("Expand"u8).X);
|
||||
var textWidth = ImUtf8.CalcTextSize(DesignPanelFlag.AdvancedCustomizations.ToName()).X;
|
||||
var tableSize = 2 * (textWidth + 2 * checkBoxWidth) + 10 * ImGui.GetStyle().CellPadding.X + 2 * ImGui.GetStyle().WindowPadding.X + 2 * ImGui.GetStyle().FrameBorderSize;
|
||||
using var table = ImUtf8.Table(label, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders, new Vector2(tableSize, 6 * ImGui.GetFrameHeight()));
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
var headerColor = ImGui.GetColorU32(ImGuiCol.TableHeaderBg);
|
||||
var checkBoxOffset = (checkBoxWidth - ImGui.GetFrameHeight()) / 2;
|
||||
ImUtf8.TableSetupColumn("Panel##1"u8, ImGuiTableColumnFlags.WidthFixed, textWidth);
|
||||
ImUtf8.TableSetupColumn("Show##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
|
||||
ImUtf8.TableSetupColumn("Expand##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
|
||||
ImUtf8.TableSetupColumn("Panel##2"u8, ImGuiTableColumnFlags.WidthFixed, textWidth);
|
||||
ImUtf8.TableSetupColumn("Show##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
|
||||
ImUtf8.TableSetupColumn("Expand##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
foreach (var panel in Enum.GetValues<DesignPanelFlag>())
|
||||
{
|
||||
using var id = ImUtf8.PushId((int)panel);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, headerColor);
|
||||
ImUtf8.TextFrameAligned(panel.ToName());
|
||||
var isShown = !hidden.HasFlag(panel);
|
||||
var isExpanded = expanded.HasFlag(panel);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset);
|
||||
if (ImUtf8.Checkbox("##show"u8, ref isShown))
|
||||
setterHide.Invoke(isShown ? hidden & ~panel : hidden | panel);
|
||||
ImUtf8.HoverTooltip(
|
||||
"Show this panel and associated functionality in all relevant tabs.\n\nToggling this off does NOT disable any functionality, just the display of it, so hide panels at your own risk."u8);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset);
|
||||
if (ImUtf8.Checkbox("##expand"u8, ref isExpanded))
|
||||
setterExpand.Invoke(isExpanded ? expanded | panel : expanded & ~panel);
|
||||
ImUtf8.HoverTooltip("Expand this panel by default in all relevant tabs."u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Glamourer/Designs/ApplicationCollection.cs
Normal file
68
Glamourer/Designs/ApplicationCollection.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using Glamourer.Api.Enums;
|
||||
using Glamourer.GameData;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public record struct ApplicationCollection(
|
||||
EquipFlag Equip,
|
||||
BonusItemFlag BonusItem,
|
||||
CustomizeFlag CustomizeRaw,
|
||||
CrestFlag Crest,
|
||||
CustomizeParameterFlag Parameters,
|
||||
MetaFlag Meta)
|
||||
{
|
||||
public static readonly ApplicationCollection All = new(EquipFlagExtensions.All, BonusExtensions.All,
|
||||
CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All);
|
||||
|
||||
public static readonly ApplicationCollection None = new(0, 0, CustomizeFlag.BodyType, 0, 0, 0);
|
||||
|
||||
public static readonly ApplicationCollection Equipment = new(EquipFlagExtensions.All, BonusExtensions.All,
|
||||
CustomizeFlag.BodyType, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState | MetaFlag.EarState);
|
||||
|
||||
public static readonly ApplicationCollection Customizations = new(0, 0, CustomizeFlagExtensions.AllRelevant, 0,
|
||||
CustomizeParameterExtensions.All, MetaFlag.Wetness);
|
||||
|
||||
public static readonly ApplicationCollection Default = new(EquipFlagExtensions.All, BonusExtensions.All,
|
||||
CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
|
||||
|
||||
public static ApplicationCollection FromKeys()
|
||||
=> (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
|
||||
{
|
||||
(false, false) => All,
|
||||
(true, true) => All,
|
||||
(true, false) => Equipment,
|
||||
(false, true) => Customizations,
|
||||
};
|
||||
|
||||
public CustomizeFlag Customize
|
||||
{
|
||||
get => CustomizeRaw;
|
||||
set => CustomizeRaw = value | CustomizeFlag.BodyType;
|
||||
}
|
||||
|
||||
public void RemoveEquip()
|
||||
{
|
||||
Equip = 0;
|
||||
BonusItem = 0;
|
||||
Crest = 0;
|
||||
Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
|
||||
}
|
||||
|
||||
public void RemoveCustomize()
|
||||
{
|
||||
Customize = 0;
|
||||
Parameters = 0;
|
||||
Meta &= MetaFlag.Wetness;
|
||||
}
|
||||
|
||||
public ApplicationCollection Restrict(ApplicationCollection old)
|
||||
=> new(old.Equip & Equip, old.BonusItem & BonusItem, (old.Customize & Customize) | CustomizeFlag.BodyType, old.Crest & Crest,
|
||||
old.Parameters & Parameters, old.Meta & Meta);
|
||||
|
||||
public ApplicationCollection CloneSecure()
|
||||
=> new(Equip & EquipFlagExtensions.All, BonusItem & BonusExtensions.All,
|
||||
(Customize & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType, Crest & CrestExtensions.AllRelevant,
|
||||
Parameters & CustomizeParameterExtensions.All, Meta & MetaExtensions.All);
|
||||
}
|
||||
71
Glamourer/Designs/ApplicationRules.cs
Normal file
71
Glamourer/Designs/ApplicationRules.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using Glamourer.Api.Enums;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.State;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public readonly struct ApplicationRules(ApplicationCollection application, bool materials)
|
||||
{
|
||||
public static readonly ApplicationRules All = new(ApplicationCollection.All, true);
|
||||
|
||||
public static ApplicationRules FromModifiers(ActorState state)
|
||||
=> FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
|
||||
|
||||
public static ApplicationRules NpcFromModifiers()
|
||||
=> NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
|
||||
|
||||
public static ApplicationRules AllButParameters(ActorState state)
|
||||
=> new(ApplicationCollection.All with { Parameters = ComputeParameters(state.ModelData, state.BaseData, All.Parameters) }, true);
|
||||
|
||||
public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift)
|
||||
{
|
||||
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
|
||||
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
|
||||
var visor = equip != 0 ? MetaFlag.VisorState : 0;
|
||||
return new ApplicationRules(new ApplicationCollection(equip, 0, customize, 0, 0, visor), false);
|
||||
}
|
||||
|
||||
public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift)
|
||||
{
|
||||
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
|
||||
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
|
||||
var bonus = equip == 0 ? 0 : BonusExtensions.All;
|
||||
var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant;
|
||||
var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All;
|
||||
var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0;
|
||||
if (equip != 0)
|
||||
meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState;
|
||||
|
||||
var collection = new ApplicationCollection(equip, bonus, customize, crest,
|
||||
ComputeParameters(state.ModelData, state.BaseData, parameters), meta);
|
||||
return new ApplicationRules(collection, equip != 0);
|
||||
}
|
||||
|
||||
public void Apply(DesignBase design)
|
||||
=> design.Application = application;
|
||||
|
||||
public EquipFlag Equip
|
||||
=> application.Equip & EquipFlagExtensions.All;
|
||||
|
||||
public CustomizeParameterFlag Parameters
|
||||
=> application.Parameters & CustomizeParameterExtensions.All;
|
||||
|
||||
public bool Materials
|
||||
=> materials;
|
||||
|
||||
private static CustomizeParameterFlag ComputeParameters(in DesignData model, in DesignData game,
|
||||
CustomizeParameterFlag baseFlags = CustomizeParameterExtensions.All)
|
||||
{
|
||||
foreach (var flag in baseFlags.Iterate())
|
||||
{
|
||||
var modelValue = model.Parameters[flag];
|
||||
var gameValue = game.Parameters[flag];
|
||||
if (modelValue.NearEqual(gameValue))
|
||||
baseFlags &= ~flag;
|
||||
}
|
||||
|
||||
return baseFlags;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,24 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Glamourer.Automation;
|
||||
using Glamourer.Designs.Links;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.State;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Notification = OtterGui.Classes.Notification;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public sealed class Design : DesignBase, ISavable
|
||||
public sealed class Design : DesignBase, ISavable, IDesignStandIn
|
||||
{
|
||||
#region Data
|
||||
internal Design(CustomizationService customize, ItemManager items)
|
||||
: base(items)
|
||||
|
||||
internal Design(CustomizeService customize, ItemManager items)
|
||||
: base(customize, items)
|
||||
{ }
|
||||
|
||||
internal Design(DesignBase other)
|
||||
|
|
@ -25,47 +28,101 @@ public sealed class Design : DesignBase, ISavable
|
|||
internal Design(Design other)
|
||||
: base(other)
|
||||
{
|
||||
Tags = Tags.ToArray();
|
||||
Description = Description;
|
||||
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
|
||||
Tags = [.. other.Tags];
|
||||
Description = other.Description;
|
||||
QuickDesign = other.QuickDesign;
|
||||
ForcedRedraw = other.ForcedRedraw;
|
||||
ResetAdvancedDyes = other.ResetAdvancedDyes;
|
||||
ResetTemporarySettings = other.ResetTemporarySettings;
|
||||
Color = other.Color;
|
||||
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
|
||||
Links = Links.Clone();
|
||||
}
|
||||
|
||||
// Metadata
|
||||
public new const int FileVersion = 1;
|
||||
public new const int FileVersion = 2;
|
||||
|
||||
public Guid Identifier { get; internal init; }
|
||||
public DateTimeOffset CreationDate { get; internal init; }
|
||||
public DateTimeOffset LastEdit { get; internal set; }
|
||||
public LowerString Name { get; internal set; } = LowerString.Empty;
|
||||
public string Description { get; internal set; } = string.Empty;
|
||||
public string[] Tags { get; internal set; } = Array.Empty<string>();
|
||||
public int Index { get; internal set; }
|
||||
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = new();
|
||||
public Guid Identifier { get; internal init; }
|
||||
public DateTimeOffset CreationDate { get; internal init; }
|
||||
public DateTimeOffset LastEdit { get; internal set; }
|
||||
public LowerString Name { get; internal set; } = LowerString.Empty;
|
||||
public string Description { get; internal set; } = string.Empty;
|
||||
public string[] Tags { get; internal set; } = [];
|
||||
public int Index { get; internal set; }
|
||||
public bool ForcedRedraw { get; internal set; }
|
||||
public bool ResetAdvancedDyes { get; internal set; }
|
||||
public bool ResetTemporarySettings { get; internal set; }
|
||||
public bool QuickDesign { get; internal set; } = true;
|
||||
public string Color { get; internal set; } = string.Empty;
|
||||
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = [];
|
||||
public LinkContainer Links { get; private set; } = [];
|
||||
|
||||
public string Incognito
|
||||
=> Identifier.ToString()[..8];
|
||||
|
||||
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
|
||||
=> LinkContainer.GetAllLinks(this).Select(t => ((IDesignStandIn)t.Link.Link, t.Link.Type, JobFlag.All));
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDesignStandIn
|
||||
|
||||
public string ResolveName(bool incognito)
|
||||
=> incognito ? Incognito : Name.Text;
|
||||
|
||||
public string SerializeName()
|
||||
=> Identifier.ToString();
|
||||
|
||||
public ref readonly DesignData GetDesignData(in DesignData baseData)
|
||||
=> ref GetDesignDataRef();
|
||||
|
||||
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
|
||||
=> Materials;
|
||||
|
||||
public bool Equals(IDesignStandIn? other)
|
||||
=> other is Design d && d.Identifier == Identifier;
|
||||
|
||||
public StateSource AssociatedSource()
|
||||
=> StateSource.Manual;
|
||||
|
||||
public void AddData(JObject _)
|
||||
{ }
|
||||
|
||||
public void ParseData(JObject _)
|
||||
{ }
|
||||
|
||||
public bool ChangeData(object data)
|
||||
=> false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization
|
||||
|
||||
public new JObject JsonSerialize()
|
||||
{
|
||||
var ret = new JObject()
|
||||
{
|
||||
["FileVersion"] = FileVersion,
|
||||
["Identifier"] = Identifier,
|
||||
["CreationDate"] = CreationDate,
|
||||
["LastEdit"] = LastEdit,
|
||||
["Name"] = Name.Text,
|
||||
["Description"] = Description,
|
||||
["Tags"] = JArray.FromObject(Tags),
|
||||
["WriteProtected"] = WriteProtected(),
|
||||
["Equipment"] = SerializeEquipment(),
|
||||
["Customize"] = SerializeCustomize(),
|
||||
["Mods"] = SerializeMods(),
|
||||
}
|
||||
;
|
||||
var ret = new JObject
|
||||
{
|
||||
["FileVersion"] = FileVersion,
|
||||
["Identifier"] = Identifier,
|
||||
["CreationDate"] = CreationDate,
|
||||
["LastEdit"] = LastEdit,
|
||||
["Name"] = Name.Text,
|
||||
["Description"] = Description,
|
||||
["ForcedRedraw"] = ForcedRedraw,
|
||||
["ResetAdvancedDyes"] = ResetAdvancedDyes,
|
||||
["ResetTemporarySettings"] = ResetTemporarySettings,
|
||||
["Color"] = Color,
|
||||
["QuickDesign"] = QuickDesign,
|
||||
["Tags"] = JArray.FromObject(Tags),
|
||||
["WriteProtected"] = WriteProtected(),
|
||||
["Equipment"] = SerializeEquipment(),
|
||||
["Bonus"] = SerializeBonusItems(),
|
||||
["Customize"] = SerializeCustomize(),
|
||||
["Parameters"] = SerializeParameters(),
|
||||
["Materials"] = SerializeMaterials(),
|
||||
["Mods"] = SerializeMods(),
|
||||
["Links"] = Links.Serialize(),
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -74,12 +131,17 @@ public sealed class Design : DesignBase, ISavable
|
|||
var ret = new JArray();
|
||||
foreach (var (mod, settings) in AssociatedMods)
|
||||
{
|
||||
var obj = new JObject()
|
||||
var obj = new JObject
|
||||
{
|
||||
["Name"] = mod.Name,
|
||||
["Directory"] = mod.DirectoryName,
|
||||
["Enabled"] = settings.Enabled,
|
||||
};
|
||||
if (settings.Remove)
|
||||
obj["Remove"] = true;
|
||||
else if (settings.ForceInherit)
|
||||
obj["Inherit"] = true;
|
||||
else
|
||||
obj["Enabled"] = settings.Enabled;
|
||||
if (settings.Enabled)
|
||||
{
|
||||
obj["Priority"] = settings.Priority;
|
||||
|
|
@ -96,24 +158,84 @@ public sealed class Design : DesignBase, ISavable
|
|||
|
||||
#region Deserialization
|
||||
|
||||
public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json)
|
||||
public static Design LoadDesign(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader,
|
||||
JObject json)
|
||||
{
|
||||
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
|
||||
return version switch
|
||||
{
|
||||
FileVersion => LoadDesignV1(customizations, items, json),
|
||||
1 => LoadDesignV1(saveService, customizations, items, linkLoader, json),
|
||||
FileVersion => LoadDesignV2(customizations, items, linkLoader, json),
|
||||
_ => throw new Exception("The design to be loaded has no valid Version."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json)
|
||||
/// <summary> The values for gloss and specular strength were switched. Swap them for all appropriate designs. </summary>
|
||||
private static Design LoadDesignV1(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader,
|
||||
JObject json)
|
||||
{
|
||||
static string[] ParseTags(JObject json)
|
||||
var design = LoadDesignV2(customizations, items, linkLoader, json);
|
||||
var materialDesignData = design.GetMaterialDataRef();
|
||||
if (materialDesignData.Values.Count == 0)
|
||||
return design;
|
||||
|
||||
var materialData = materialDesignData.Clone();
|
||||
// Guesstimate whether to migrate material rows:
|
||||
// Update 1.3.0.10 released at that time, so any design last updated before that can be migrated.
|
||||
if (design.LastEdit <= new DateTime(2024, 8, 7, 16, 0, 0, DateTimeKind.Utc))
|
||||
{
|
||||
var tags = json["Tags"]?.ToObject<string[]>() ?? Array.Empty<string>();
|
||||
return tags.OrderBy(t => t).Distinct().ToArray();
|
||||
Migrate("because it was saved the wrong way around before 1.3.0.10, and this design was not changed since that release.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasNegativeGloss = false;
|
||||
var hasNonPositiveGloss = false;
|
||||
var specularLarger = 0;
|
||||
foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max()))
|
||||
{
|
||||
hasNegativeGloss |= value.Value.GlossStrength < 0;
|
||||
hasNonPositiveGloss |= value.Value.GlossStrength <= 0;
|
||||
if (value.Value.SpecularStrength > value.Value.GlossStrength)
|
||||
++specularLarger;
|
||||
}
|
||||
|
||||
// If there is any negative gloss, this is wrong and can be migrated.
|
||||
if (hasNegativeGloss)
|
||||
Migrate("because it had a negative Gloss value, which is not supported and thus probably outdated.");
|
||||
// If there is any non-positive Gloss and some specular values that are larger, it is probably wrong and can be migrated.
|
||||
else if (hasNonPositiveGloss && specularLarger > 0)
|
||||
Migrate("because it had a zero Gloss value, and at least one Specular Strength larger than the Gloss, which is unusual.");
|
||||
// If most of the specular strengths are larger, it is probably wrong and can be migrated.
|
||||
else if (specularLarger > materialData.Values.Count / 2)
|
||||
Migrate("because most of its Specular Strength values were larger than the Gloss values, which is unusual.");
|
||||
}
|
||||
|
||||
return design;
|
||||
|
||||
void Migrate(string reason)
|
||||
{
|
||||
materialDesignData.Clear();
|
||||
foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max()))
|
||||
{
|
||||
var gloss = Math.Clamp(value.Value.SpecularStrength, 0, (float)Half.MaxValue);
|
||||
var specularStrength = Math.Clamp(value.Value.GlossStrength, 0, (float)Half.MaxValue);
|
||||
var colorRow = value.Value with
|
||||
{
|
||||
GlossStrength = gloss,
|
||||
SpecularStrength = specularStrength,
|
||||
};
|
||||
materialDesignData.AddOrUpdateValue(MaterialValueIndex.FromKey(key), value with { Value = colorRow });
|
||||
}
|
||||
|
||||
Glamourer.Messager.AddMessage(new Notification(
|
||||
$"Swapped Gloss and Specular Strength in {materialDesignData.Values.Count} Rows in design {design.Incognito} {reason}",
|
||||
NotificationType.Info));
|
||||
saveService.Save(SaveType.ImmediateSync, design);
|
||||
}
|
||||
}
|
||||
|
||||
private static Design LoadDesignV2(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json)
|
||||
{
|
||||
var creationDate = json["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
|
||||
|
||||
var design = new Design(customizations, items)
|
||||
|
|
@ -124,14 +246,29 @@ public sealed class Design : DesignBase, ISavable
|
|||
Description = json["Description"]?.ToObject<string>() ?? string.Empty,
|
||||
Tags = ParseTags(json),
|
||||
LastEdit = json["LastEdit"]?.ToObject<DateTimeOffset>() ?? creationDate,
|
||||
QuickDesign = json["QuickDesign"]?.ToObject<bool>() ?? true,
|
||||
};
|
||||
if (design.LastEdit < creationDate)
|
||||
design.LastEdit = creationDate;
|
||||
design.SetWriteProtected(json["WriteProtected"]?.ToObject<bool>() ?? false);
|
||||
LoadCustomize(customizations, json["Customize"], design, design.Name, true, false);
|
||||
LoadEquip(items, json["Equipment"], design, design.Name, true);
|
||||
LoadBonus(items, design, json["Bonus"]);
|
||||
LoadMods(json["Mods"], design);
|
||||
LoadParameters(json["Parameters"], design, design.Name);
|
||||
LoadMaterials(json["Materials"], design, design.Name);
|
||||
LoadLinks(linkLoader, json["Links"], design);
|
||||
design.Color = json["Color"]?.ToObject<string>() ?? string.Empty;
|
||||
design.ForcedRedraw = json["ForcedRedraw"]?.ToObject<bool>() ?? false;
|
||||
design.ResetAdvancedDyes = json["ResetAdvancedDyes"]?.ToObject<bool>() ?? false;
|
||||
design.ResetTemporarySettings = json["ResetTemporarySettings"]?.ToObject<bool>() ?? false;
|
||||
return design;
|
||||
|
||||
static string[] ParseTags(JObject json)
|
||||
{
|
||||
var tags = json["Tags"]?.ToObject<string[]>() ?? [];
|
||||
return tags.OrderBy(t => t).Distinct().ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadMods(JToken? mods, Design design)
|
||||
|
|
@ -150,16 +287,42 @@ public sealed class Design : DesignBase, ISavable
|
|||
continue;
|
||||
}
|
||||
|
||||
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, string[]>>() ?? new Dictionary<string, string[]>();
|
||||
var settings = new SortedList<string, IList<string>>(settingsDict.Count);
|
||||
var forceInherit = tok["Inherit"]?.ToObject<bool>() ?? false;
|
||||
var removeSetting = tok["Remove"]?.ToObject<bool>() ?? false;
|
||||
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, List<string>>>() ?? [];
|
||||
var settings = new Dictionary<string, List<string>>(settingsDict.Count);
|
||||
foreach (var (key, value) in settingsDict)
|
||||
settings.Add(key, value);
|
||||
var priority = tok["Priority"]?.ToObject<int>() ?? 0;
|
||||
if (!design.AssociatedMods.TryAdd(new Mod(name, directory), new ModSettings(settings, priority, enabled.Value)))
|
||||
if (!design.AssociatedMods.TryAdd(new Mod(name, directory),
|
||||
new ModSettings(settings, priority, enabled.Value, forceInherit, removeSetting)))
|
||||
Glamourer.Messager.NotificationMessage("The loaded design contains a mod more than once, skipped.", NotificationType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadLinks(DesignLinkLoader linkLoader, JToken? links, Design design)
|
||||
{
|
||||
if (links is not JObject obj)
|
||||
return;
|
||||
|
||||
Parse(obj["Before"] as JArray, LinkOrder.Before);
|
||||
Parse(obj["After"] as JArray, LinkOrder.After);
|
||||
return;
|
||||
|
||||
void Parse(JArray? array, LinkOrder order)
|
||||
{
|
||||
if (array == null)
|
||||
return;
|
||||
|
||||
foreach (var jObj in array.OfType<JObject>())
|
||||
{
|
||||
var identifier = jObj["Design"]?.ToObject<Guid>() ?? throw new ArgumentNullException(nameof(design));
|
||||
var type = (ApplicationType)(jObj["Type"]?.ToObject<uint>() ?? 0);
|
||||
linkLoader.AddObject(design, new LinkData(identifier, type, order));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISavable
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Glamourer.Customization;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Structs;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
|
|
@ -16,172 +14,215 @@ public class DesignBase
|
|||
{
|
||||
public const int FileVersion = 1;
|
||||
|
||||
internal DesignBase(ItemManager items)
|
||||
private DesignData _designData = new();
|
||||
private readonly DesignMaterialManager _materials = new();
|
||||
|
||||
/// <summary> For read-only information about custom material color changes. </summary>
|
||||
public IReadOnlyList<(uint, MaterialValueDesign)> Materials
|
||||
=> _materials.Values;
|
||||
|
||||
/// <summary> To make it clear something is edited here. </summary>
|
||||
public DesignMaterialManager GetMaterialDataRef()
|
||||
=> _materials;
|
||||
|
||||
/// <summary> For read-only information about the actual design. </summary>
|
||||
public ref readonly DesignData DesignData
|
||||
=> ref _designData;
|
||||
|
||||
/// <summary> To make it clear that something is edited here. </summary>
|
||||
public ref DesignData GetDesignDataRef()
|
||||
=> ref _designData;
|
||||
|
||||
internal DesignBase(CustomizeService customize, ItemManager items)
|
||||
{
|
||||
DesignData.SetDefaultEquipment(items);
|
||||
_designData.SetDefaultEquipment(items);
|
||||
CustomizeSet = SetCustomizationSet(customize);
|
||||
}
|
||||
|
||||
/// <summary> Used when importing .cma or .chara files. </summary>
|
||||
internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags,
|
||||
BonusItemFlag bonusFlags)
|
||||
{
|
||||
_designData = designData;
|
||||
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
|
||||
Application.Equip = equipFlags & EquipFlagExtensions.All;
|
||||
Application.BonusItem = bonusFlags & BonusExtensions.All;
|
||||
Application.Meta = 0;
|
||||
CustomizeSet = SetCustomizationSet(customize);
|
||||
}
|
||||
|
||||
internal DesignBase(DesignBase clone)
|
||||
{
|
||||
DesignData = clone.DesignData;
|
||||
ApplyCustomize = clone.ApplyCustomize & CustomizeFlagExtensions.AllRelevant;
|
||||
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
|
||||
_designFlags = clone._designFlags & (DesignFlags)0x0F;
|
||||
_designData = clone._designData;
|
||||
_materials = clone._materials.Clone();
|
||||
CustomizeSet = clone.CustomizeSet;
|
||||
Application = clone.Application.CloneSecure();
|
||||
}
|
||||
|
||||
internal DesignData DesignData = new();
|
||||
/// <summary> Ensure that the customization set is updated when the design data changes. </summary>
|
||||
internal void SetDesignData(CustomizeService customize, in DesignData other)
|
||||
{
|
||||
_designData = other;
|
||||
CustomizeSet = SetCustomizationSet(customize);
|
||||
}
|
||||
|
||||
#region Application Data
|
||||
|
||||
[Flags]
|
||||
private enum DesignFlags : byte
|
||||
public CustomizeSet CustomizeSet { get; private set; }
|
||||
|
||||
public ApplicationCollection Application = ApplicationCollection.Default;
|
||||
|
||||
internal CustomizeFlag ApplyCustomize
|
||||
{
|
||||
ApplyHatVisible = 0x01,
|
||||
ApplyVisorState = 0x02,
|
||||
ApplyWeaponVisible = 0x04,
|
||||
ApplyWetness = 0x08,
|
||||
WriteProtected = 0x10,
|
||||
get => Application.Customize.FixApplication(CustomizeSet);
|
||||
set => Application.Customize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType;
|
||||
}
|
||||
|
||||
internal CustomizeFlag ApplyCustomize = CustomizeFlagExtensions.AllRelevant;
|
||||
internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
|
||||
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
|
||||
internal CustomizeFlag ApplyCustomizeExcludingBodyType
|
||||
=> Application.Customize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType;
|
||||
|
||||
public bool DoApplyHatVisible()
|
||||
=> _designFlags.HasFlag(DesignFlags.ApplyHatVisible);
|
||||
private bool _writeProtected;
|
||||
|
||||
public bool DoApplyVisorToggle()
|
||||
=> _designFlags.HasFlag(DesignFlags.ApplyVisorState);
|
||||
public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize)
|
||||
{
|
||||
if (customize.Equals(_designData.Customize))
|
||||
return false;
|
||||
|
||||
public bool DoApplyWeaponVisible()
|
||||
=> _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible);
|
||||
_designData.Customize = customize;
|
||||
CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DoApplyWetness()
|
||||
=> _designFlags.HasFlag(DesignFlags.ApplyWetness);
|
||||
public bool DoApplyMeta(MetaIndex index)
|
||||
=> Application.Meta.HasFlag(index.ToFlag());
|
||||
|
||||
public bool WriteProtected()
|
||||
=> _designFlags.HasFlag(DesignFlags.WriteProtected);
|
||||
=> _writeProtected;
|
||||
|
||||
public bool SetApplyHatVisible(bool value)
|
||||
public bool SetApplyMeta(MetaIndex index, bool value)
|
||||
{
|
||||
var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible;
|
||||
if (newFlag == _designFlags)
|
||||
var newFlag = value ? Application.Meta | index.ToFlag() : Application.Meta & ~index.ToFlag();
|
||||
if (newFlag == Application.Meta)
|
||||
return false;
|
||||
|
||||
_designFlags = newFlag;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetApplyVisorToggle(bool value)
|
||||
{
|
||||
var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState;
|
||||
if (newFlag == _designFlags)
|
||||
return false;
|
||||
|
||||
_designFlags = newFlag;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetApplyWeaponVisible(bool value)
|
||||
{
|
||||
var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible;
|
||||
if (newFlag == _designFlags)
|
||||
return false;
|
||||
|
||||
_designFlags = newFlag;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetApplyWetness(bool value)
|
||||
{
|
||||
var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness;
|
||||
if (newFlag == _designFlags)
|
||||
return false;
|
||||
|
||||
_designFlags = newFlag;
|
||||
Application.Meta = newFlag;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetWriteProtected(bool value)
|
||||
{
|
||||
var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected;
|
||||
if (newFlag == _designFlags)
|
||||
if (value == _writeProtected)
|
||||
return false;
|
||||
|
||||
_designFlags = newFlag;
|
||||
_writeProtected = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DoApplyEquip(EquipSlot slot)
|
||||
=> ApplyEquip.HasFlag(slot.ToFlag());
|
||||
=> Application.Equip.HasFlag(slot.ToFlag());
|
||||
|
||||
public bool DoApplyStain(EquipSlot slot)
|
||||
=> ApplyEquip.HasFlag(slot.ToStainFlag());
|
||||
=> Application.Equip.HasFlag(slot.ToStainFlag());
|
||||
|
||||
public bool DoApplyCustomize(CustomizeIndex idx)
|
||||
=> idx is not CustomizeIndex.Race and not CustomizeIndex.BodyType && ApplyCustomize.HasFlag(idx.ToFlag());
|
||||
=> Application.Customize.HasFlag(idx.ToFlag());
|
||||
|
||||
public bool DoApplyCrest(CrestFlag slot)
|
||||
=> Application.Crest.HasFlag(slot);
|
||||
|
||||
public bool DoApplyParameter(CustomizeParameterFlag flag)
|
||||
=> Application.Parameters.HasFlag(flag);
|
||||
|
||||
public bool DoApplyBonusItem(BonusItemFlag slot)
|
||||
=> Application.BonusItem.HasFlag(slot);
|
||||
|
||||
internal bool SetApplyEquip(EquipSlot slot, bool value)
|
||||
{
|
||||
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
|
||||
if (newValue == ApplyEquip)
|
||||
var newValue = value ? Application.Equip | slot.ToFlag() : Application.Equip & ~slot.ToFlag();
|
||||
if (newValue == Application.Equip)
|
||||
return false;
|
||||
|
||||
ApplyEquip = newValue;
|
||||
Application.Equip = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool SetApplyBonusItem(BonusItemFlag slot, bool value)
|
||||
{
|
||||
var newValue = value ? Application.BonusItem | slot : Application.BonusItem & ~slot;
|
||||
if (newValue == Application.BonusItem)
|
||||
return false;
|
||||
|
||||
Application.BonusItem = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool SetApplyStain(EquipSlot slot, bool value)
|
||||
{
|
||||
var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag();
|
||||
if (newValue == ApplyEquip)
|
||||
var newValue = value ? Application.Equip | slot.ToStainFlag() : Application.Equip & ~slot.ToStainFlag();
|
||||
if (newValue == Application.Equip)
|
||||
return false;
|
||||
|
||||
ApplyEquip = newValue;
|
||||
Application.Equip = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
|
||||
{
|
||||
var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag();
|
||||
if (newValue == ApplyCustomize)
|
||||
var newValue = value ? Application.Customize | idx.ToFlag() : Application.Customize & ~idx.ToFlag();
|
||||
if (newValue == Application.Customize)
|
||||
return false;
|
||||
|
||||
ApplyCustomize = newValue;
|
||||
Application.Customize = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FixCustomizeApplication(CustomizationService service, CustomizeFlag flags)
|
||||
=> FixCustomizeApplication(service.AwaitedService.GetList(DesignData.Customize.Clan, DesignData.Customize.Gender), flags);
|
||||
internal bool SetApplyCrest(CrestFlag slot, bool value)
|
||||
{
|
||||
var newValue = value ? Application.Crest | slot : Application.Crest & ~slot;
|
||||
if (newValue == Application.Crest)
|
||||
return false;
|
||||
|
||||
public void FixCustomizeApplication(CustomizationSet set, CustomizeFlag flags)
|
||||
=> ApplyCustomize = flags.FixApplication(set);
|
||||
Application.Crest = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags)
|
||||
=> new(this, equipFlags, customizeFlags);
|
||||
internal bool SetApplyParameter(CustomizeParameterFlag flag, bool value)
|
||||
{
|
||||
var newValue = value ? Application.Parameters | flag : Application.Parameters & ~flag;
|
||||
if (newValue == Application.Parameters)
|
||||
return false;
|
||||
|
||||
Application.Parameters = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<string> FilteredItemNames
|
||||
=> _designData.FilteredItemNames(Application.Equip, Application.BonusItem);
|
||||
|
||||
internal FlagRestrictionResetter TemporarilyRestrictApplication(ApplicationCollection restrictions)
|
||||
=> new(this, restrictions);
|
||||
|
||||
internal readonly struct FlagRestrictionResetter : IDisposable
|
||||
{
|
||||
private readonly DesignBase _design;
|
||||
private readonly EquipFlag _oldEquipFlags;
|
||||
private readonly CustomizeFlag _oldCustomizeFlags;
|
||||
private readonly DesignBase _design;
|
||||
private readonly ApplicationCollection _oldFlags;
|
||||
|
||||
public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags)
|
||||
public FlagRestrictionResetter(DesignBase d, ApplicationCollection restrictions)
|
||||
{
|
||||
_design = d;
|
||||
_oldEquipFlags = d.ApplyEquip;
|
||||
_oldCustomizeFlags = d.ApplyCustomize;
|
||||
d.ApplyEquip &= equipFlags;
|
||||
d.ApplyCustomize &= customizeFlags;
|
||||
_design = d;
|
||||
_oldFlags = d.Application;
|
||||
_design.Application = restrictions.Restrict(_oldFlags);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_design.ApplyEquip = _oldEquipFlags;
|
||||
_design.ApplyCustomize = _oldCustomizeFlags;
|
||||
}
|
||||
=> _design.Application = _oldFlags;
|
||||
}
|
||||
|
||||
private CustomizeSet SetCustomizationSet(CustomizeService customize)
|
||||
=> !_designData.IsHuman
|
||||
? customize.Manager.GetSet(SubRace.Midlander, Gender.Male)
|
||||
: customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization
|
||||
|
|
@ -192,39 +233,62 @@ public class DesignBase
|
|||
{
|
||||
["FileVersion"] = FileVersion,
|
||||
["Equipment"] = SerializeEquipment(),
|
||||
["Bonus"] = SerializeBonusItems(),
|
||||
["Customize"] = SerializeCustomize(),
|
||||
["Parameters"] = SerializeParameters(),
|
||||
["Materials"] = SerializeMaterials(),
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected JObject SerializeEquipment()
|
||||
{
|
||||
static JObject Serialize(CustomItemId id, StainId stain, bool apply, bool applyStain)
|
||||
=> new()
|
||||
{
|
||||
["ItemId"] = id.Id,
|
||||
["Stain"] = stain.Id,
|
||||
["Apply"] = apply,
|
||||
["ApplyStain"] = applyStain,
|
||||
};
|
||||
|
||||
var ret = new JObject();
|
||||
if (DesignData.IsHuman)
|
||||
if (_designData.IsHuman)
|
||||
{
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
|
||||
{
|
||||
var item = DesignData.Item(slot);
|
||||
var stain = DesignData.Stain(slot);
|
||||
ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot));
|
||||
var item = _designData.Item(slot);
|
||||
var stains = _designData.Stain(slot);
|
||||
var crestSlot = slot.ToCrestFlag();
|
||||
var crest = _designData.Crest(crestSlot);
|
||||
ret[slot.ToString()] = Serialize(item.Id, stains, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot));
|
||||
}
|
||||
|
||||
ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply");
|
||||
ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply");
|
||||
ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply");
|
||||
ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply");
|
||||
ret["VieraEars"] = new QuadBool(_designData.AreEarsVisible(), DoApplyMeta(MetaIndex.EarState)).ToJObject("Show", "Apply");
|
||||
ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply");
|
||||
ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply");
|
||||
}
|
||||
else
|
||||
{
|
||||
ret["Array"] = DesignData.WriteEquipmentBytesBase64();
|
||||
ret["Array"] = _designData.WriteEquipmentBytesBase64();
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
static JObject Serialize(CustomItemId id, StainIds stains, bool crest, bool apply, bool applyStain, bool applyCrest)
|
||||
=> stains.AddToObject(new JObject
|
||||
{
|
||||
["ItemId"] = id.Id,
|
||||
["Crest"] = crest,
|
||||
["Apply"] = apply,
|
||||
["ApplyStain"] = applyStain,
|
||||
["ApplyCrest"] = applyCrest,
|
||||
});
|
||||
}
|
||||
|
||||
protected JObject SerializeBonusItems()
|
||||
{
|
||||
var ret = new JObject();
|
||||
foreach (var slot in BonusExtensions.AllFlags)
|
||||
{
|
||||
var item = _designData.BonusItem(slot);
|
||||
ret[slot.ToString()] = new JObject()
|
||||
{
|
||||
["BonusId"] = item.Id.Id,
|
||||
["Apply"] = DoApplyBonusItem(slot),
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
|
@ -234,17 +298,17 @@ public class DesignBase
|
|||
{
|
||||
var ret = new JObject()
|
||||
{
|
||||
["ModelId"] = DesignData.ModelId,
|
||||
["ModelId"] = _designData.ModelId,
|
||||
};
|
||||
|
||||
var customize = DesignData.Customize;
|
||||
if (DesignData.IsHuman)
|
||||
var customize = _designData.Customize;
|
||||
if (_designData.IsHuman)
|
||||
foreach (var idx in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
ret[idx.ToString()] = new JObject()
|
||||
{
|
||||
["Value"] = customize[idx].Value,
|
||||
["Apply"] = DoApplyCustomize(idx),
|
||||
["Apply"] = Application.Customize.HasFlag(idx.ToFlag()),
|
||||
};
|
||||
}
|
||||
else
|
||||
|
|
@ -252,18 +316,105 @@ public class DesignBase
|
|||
|
||||
ret["Wetness"] = new JObject()
|
||||
{
|
||||
["Value"] = DesignData.IsWet(),
|
||||
["Apply"] = DoApplyWetness(),
|
||||
["Value"] = _designData.IsWet(),
|
||||
["Apply"] = DoApplyMeta(MetaIndex.Wetness),
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected JObject SerializeParameters()
|
||||
{
|
||||
var ret = new JObject();
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.ValueFlags)
|
||||
{
|
||||
ret[flag.ToString()] = new JObject()
|
||||
{
|
||||
["Value"] = DesignData.Parameters[flag][0],
|
||||
["Apply"] = DoApplyParameter(flag),
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.PercentageFlags)
|
||||
{
|
||||
ret[flag.ToString()] = new JObject()
|
||||
{
|
||||
["Percentage"] = DesignData.Parameters[flag][0],
|
||||
["Apply"] = DoApplyParameter(flag),
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.RgbFlags)
|
||||
{
|
||||
ret[flag.ToString()] = new JObject()
|
||||
{
|
||||
["Red"] = DesignData.Parameters[flag][0],
|
||||
["Green"] = DesignData.Parameters[flag][1],
|
||||
["Blue"] = DesignData.Parameters[flag][2],
|
||||
["Apply"] = DoApplyParameter(flag),
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.RgbaFlags)
|
||||
{
|
||||
ret[flag.ToString()] = new JObject()
|
||||
{
|
||||
["Red"] = DesignData.Parameters[flag][0],
|
||||
["Green"] = DesignData.Parameters[flag][1],
|
||||
["Blue"] = DesignData.Parameters[flag][2],
|
||||
["Alpha"] = DesignData.Parameters[flag][3],
|
||||
["Apply"] = DoApplyParameter(flag),
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected JObject SerializeMaterials()
|
||||
{
|
||||
var ret = new JObject();
|
||||
foreach (var (key, value) in Materials)
|
||||
ret[key.ToString("X16")] = JToken.FromObject(value);
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected static void LoadMaterials(JToken? materials, DesignBase design, string name)
|
||||
{
|
||||
if (materials is not JObject obj)
|
||||
return;
|
||||
|
||||
design.GetMaterialDataRef().Clear();
|
||||
foreach (var (key, value) in obj.Properties().Zip(obj.PropertyValues()))
|
||||
{
|
||||
try
|
||||
{
|
||||
var k = uint.Parse(key.Name, NumberStyles.HexNumber);
|
||||
var v = value.ToObject<MaterialValueDesign>();
|
||||
if (!MaterialValueIndex.FromKey(k, out _))
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.",
|
||||
NotificationType.Warning);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!design.GetMaterialDataRef().TryAddValue(MaterialValueIndex.FromKey(k), v))
|
||||
Glamourer.Messager.NotificationMessage($"Duplicate material value key {k} for design {name}, skipped.",
|
||||
NotificationType.Warning);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage(ex, $"Error parsing material value for design {name}, skipped",
|
||||
NotificationType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization
|
||||
|
||||
public static DesignBase LoadDesignBase(CustomizationService customizations, ItemManager items, JObject json)
|
||||
public static DesignBase LoadDesignBase(CustomizeService customizations, ItemManager items, JObject json)
|
||||
{
|
||||
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
|
||||
return version switch
|
||||
|
|
@ -273,99 +424,219 @@ public class DesignBase
|
|||
};
|
||||
}
|
||||
|
||||
private static DesignBase LoadDesignV1Base(CustomizationService customizations, ItemManager items, JObject json)
|
||||
private static DesignBase LoadDesignV1Base(CustomizeService customizations, ItemManager items, JObject json)
|
||||
{
|
||||
var ret = new DesignBase(items);
|
||||
var ret = new DesignBase(customizations, items);
|
||||
LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true);
|
||||
LoadEquip(items, json["Equipment"], ret, "Temporary Design", true);
|
||||
LoadParameters(json["Parameters"], ret, "Temporary Design");
|
||||
LoadMaterials(json["Materials"], ret, "Temporary Design");
|
||||
LoadBonus(items, ret, json["Bonus"]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected static void LoadBonus(ItemManager items, DesignBase design, JToken? json)
|
||||
{
|
||||
if (json is not JObject)
|
||||
{
|
||||
design.Application.BonusItem = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var slot in BonusExtensions.AllFlags)
|
||||
{
|
||||
if (json[slot.ToString()] is not JObject itemJson)
|
||||
{
|
||||
design.Application.BonusItem &= ~slot;
|
||||
design.GetDesignDataRef().SetBonusItem(slot, EquipItem.BonusItemNothing(slot));
|
||||
continue;
|
||||
}
|
||||
|
||||
design.SetApplyBonusItem(slot, itemJson["Apply"]?.ToObject<bool>() ?? false);
|
||||
var id = itemJson["BonusId"]?.ToObject<ulong>() ?? 0;
|
||||
var item = items.Resolve(slot, id);
|
||||
design.GetDesignDataRef().SetBonusItem(slot, item);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void LoadParameters(JToken? parameters, DesignBase design, string name)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
design.Application.Parameters = 0;
|
||||
design.GetDesignDataRef().Parameters = default;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.ValueFlags)
|
||||
{
|
||||
if (!TryGetToken(flag, out var token))
|
||||
continue;
|
||||
|
||||
var value = token["Value"]?.ToObject<float>() ?? 0f;
|
||||
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(value);
|
||||
}
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.PercentageFlags)
|
||||
{
|
||||
if (!TryGetToken(flag, out var token))
|
||||
continue;
|
||||
|
||||
var value = token["Percentage"]?.ToObject<float>() ?? 0f;
|
||||
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(value);
|
||||
}
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.RgbFlags)
|
||||
{
|
||||
if (!TryGetToken(flag, out var token))
|
||||
continue;
|
||||
|
||||
var r = token["Red"]?.ToObject<float>() ?? 0f;
|
||||
var g = token["Green"]?.ToObject<float>() ?? 0f;
|
||||
var b = token["Blue"]?.ToObject<float>() ?? 0f;
|
||||
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(r, g, b);
|
||||
}
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.RgbaFlags)
|
||||
{
|
||||
if (!TryGetToken(flag, out var token))
|
||||
continue;
|
||||
|
||||
var r = token["Red"]?.ToObject<float>() ?? 0f;
|
||||
var g = token["Green"]?.ToObject<float>() ?? 0f;
|
||||
var b = token["Blue"]?.ToObject<float>() ?? 0f;
|
||||
var a = token["Alpha"]?.ToObject<float>() ?? 0f;
|
||||
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(r, g, b, a);
|
||||
}
|
||||
|
||||
MigrateLipOpacity();
|
||||
return;
|
||||
|
||||
// Load the token and set application.
|
||||
bool TryGetToken(CustomizeParameterFlag flag, [NotNullWhen(true)] out JToken? token)
|
||||
{
|
||||
token = parameters[flag.ToString()];
|
||||
if (token != null)
|
||||
{
|
||||
var apply = token["Apply"]?.ToObject<bool>() ?? false;
|
||||
design.SetApplyParameter(flag, apply);
|
||||
return true;
|
||||
}
|
||||
|
||||
design.Application.Parameters &= ~flag;
|
||||
design.GetDesignDataRef().Parameters[flag] = CustomizeParameterValue.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
void MigrateLipOpacity()
|
||||
{
|
||||
var token = parameters["LipOpacity"]?["Percentage"]?.ToObject<float>();
|
||||
var actualToken = parameters[CustomizeParameterFlag.LipDiffuse.ToString()]?["Alpha"];
|
||||
if (token != null && actualToken == null)
|
||||
design.GetDesignDataRef().Parameters.LipDiffuse.W = token.Value;
|
||||
}
|
||||
}
|
||||
|
||||
protected static void LoadEquip(ItemManager items, JToken? equip, DesignBase design, string name, bool allowUnknown)
|
||||
{
|
||||
if (equip == null)
|
||||
{
|
||||
design.DesignData.SetDefaultEquipment(items);
|
||||
design._designData.SetDefaultEquipment(items);
|
||||
Glamourer.Messager.NotificationMessage("The loaded design does not contain any equipment data, reset to default.",
|
||||
NotificationType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!design.DesignData.IsHuman)
|
||||
if (!design._designData.IsHuman)
|
||||
{
|
||||
var textArray = equip["Array"]?.ToObject<string>() ?? string.Empty;
|
||||
design.DesignData.SetEquipmentBytesFromBase64(textArray);
|
||||
design._designData.SetEquipmentBytesFromBase64(textArray);
|
||||
return;
|
||||
}
|
||||
|
||||
static (CustomItemId, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
var id = item?["ItemId"]?.ToObject<ulong>() ?? ItemManager.NothingId(slot).Id;
|
||||
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
|
||||
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
|
||||
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
|
||||
return (id, stain, apply, applyStain);
|
||||
var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]);
|
||||
|
||||
PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown));
|
||||
PrintWarning(items.ValidateStain(stains, out stains, allowUnknown));
|
||||
var crestSlot = slot.ToCrestFlag();
|
||||
design._designData.SetItem(slot, item);
|
||||
design._designData.SetStain(slot, stains);
|
||||
design._designData.SetCrest(crestSlot, crest);
|
||||
design.SetApplyEquip(slot, apply);
|
||||
design.SetApplyStain(slot, applyStain);
|
||||
design.SetApplyCrest(crestSlot, applyCrest);
|
||||
}
|
||||
|
||||
{
|
||||
var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
|
||||
if (id == ItemManager.NothingId(EquipSlot.MainHand))
|
||||
id = items.DefaultSword.ItemId;
|
||||
var (idOff, stainsOff, crestOff, applyOff, applyStainOff, applyCrestOff) =
|
||||
ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
|
||||
if (id == ItemManager.NothingId(EquipSlot.OffHand))
|
||||
id = ItemManager.NothingId(FullEquipType.Shield);
|
||||
|
||||
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown));
|
||||
PrintWarning(items.ValidateStain(stains, out stains, allowUnknown));
|
||||
PrintWarning(items.ValidateStain(stainsOff, out stainsOff, allowUnknown));
|
||||
design._designData.SetItem(EquipSlot.MainHand, main);
|
||||
design._designData.SetItem(EquipSlot.OffHand, off);
|
||||
design._designData.SetStain(EquipSlot.MainHand, stains);
|
||||
design._designData.SetStain(EquipSlot.OffHand, stainsOff);
|
||||
design._designData.SetCrest(CrestFlag.MainHand, crest);
|
||||
design._designData.SetCrest(CrestFlag.OffHand, crestOff);
|
||||
design.SetApplyEquip(EquipSlot.MainHand, apply);
|
||||
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
|
||||
design.SetApplyStain(EquipSlot.MainHand, applyStain);
|
||||
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
|
||||
design.SetApplyCrest(CrestFlag.MainHand, applyCrest);
|
||||
design.SetApplyCrest(CrestFlag.OffHand, applyCrestOff);
|
||||
}
|
||||
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
|
||||
design.SetApplyMeta(MetaIndex.HatState, metaValue.Enabled);
|
||||
design._designData.SetHatVisible(metaValue.ForcedValue);
|
||||
|
||||
metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
|
||||
design.SetApplyMeta(MetaIndex.WeaponState, metaValue.Enabled);
|
||||
design._designData.SetWeaponVisible(metaValue.ForcedValue);
|
||||
|
||||
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
|
||||
design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled);
|
||||
design._designData.SetVisor(metaValue.ForcedValue);
|
||||
|
||||
metaValue = QuadBool.FromJObject(equip["VieraEars"], "Show", "Apply", QuadBool.NullTrue);
|
||||
design.SetApplyMeta(MetaIndex.EarState, metaValue.Enabled);
|
||||
design._designData.SetEarsVisible(metaValue.ForcedValue);
|
||||
return;
|
||||
|
||||
void PrintWarning(string msg)
|
||||
{
|
||||
if (msg.Length > 0 && name != "Temporary Design")
|
||||
Glamourer.Messager.NotificationMessage($"{msg} ({name})", NotificationType.Warning);
|
||||
}
|
||||
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
static (CustomItemId, StainIds, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item)
|
||||
{
|
||||
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
|
||||
|
||||
PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown));
|
||||
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown));
|
||||
design.DesignData.SetItem(slot, item);
|
||||
design.DesignData.SetStain(slot, stain);
|
||||
design.SetApplyEquip(slot, apply);
|
||||
design.SetApplyStain(slot, applyStain);
|
||||
var id = item?["ItemId"]?.ToObject<ulong>() ?? ItemManager.NothingId(slot).Id;
|
||||
var stains = StainIds.ParseFromObject(item as JObject);
|
||||
var crest = item?["Crest"]?.ToObject<bool>() ?? false;
|
||||
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
|
||||
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
|
||||
var applyCrest = item?["ApplyCrest"]?.ToObject<bool>() ?? false;
|
||||
return (id, stains, crest, apply, applyStain, applyCrest);
|
||||
}
|
||||
|
||||
{
|
||||
var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
|
||||
if (id == ItemManager.NothingId(EquipSlot.MainHand))
|
||||
id = items.DefaultSword.ItemId;
|
||||
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
|
||||
if (id == ItemManager.NothingId(EquipSlot.OffHand))
|
||||
id = ItemManager.NothingId(FullEquipType.Shield);
|
||||
|
||||
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown));
|
||||
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown));
|
||||
PrintWarning(items.ValidateStain(stainOff, out stainOff, allowUnknown));
|
||||
design.DesignData.SetItem(EquipSlot.MainHand, main);
|
||||
design.DesignData.SetItem(EquipSlot.OffHand, off);
|
||||
design.DesignData.SetStain(EquipSlot.MainHand, stain);
|
||||
design.DesignData.SetStain(EquipSlot.OffHand, stainOff);
|
||||
design.SetApplyEquip(EquipSlot.MainHand, apply);
|
||||
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
|
||||
design.SetApplyStain(EquipSlot.MainHand, applyStain);
|
||||
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
|
||||
}
|
||||
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
|
||||
design.SetApplyHatVisible(metaValue.Enabled);
|
||||
design.DesignData.SetHatVisible(metaValue.ForcedValue);
|
||||
|
||||
metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
|
||||
design.SetApplyWeaponVisible(metaValue.Enabled);
|
||||
design.DesignData.SetWeaponVisible(metaValue.ForcedValue);
|
||||
|
||||
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
|
||||
design.SetApplyVisorToggle(metaValue.Enabled);
|
||||
design.DesignData.SetVisor(metaValue.ForcedValue);
|
||||
}
|
||||
|
||||
protected static void LoadCustomize(CustomizationService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
|
||||
protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
|
||||
bool allowUnknown)
|
||||
{
|
||||
if (json == null)
|
||||
{
|
||||
design.DesignData.ModelId = 0;
|
||||
design.DesignData.IsHuman = true;
|
||||
design.DesignData.Customize = Customize.Default;
|
||||
design._designData.ModelId = 0;
|
||||
design._designData.IsHuman = true;
|
||||
design.SetCustomize(customizations, CustomizeArray.Default);
|
||||
Glamourer.Messager.NotificationMessage("The loaded design does not contain any customization data, reset to default.",
|
||||
NotificationType.Warning);
|
||||
return;
|
||||
|
|
@ -380,21 +651,23 @@ public class DesignBase
|
|||
}
|
||||
|
||||
var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse);
|
||||
design.DesignData.SetIsWet(wetness.ForcedValue);
|
||||
design.SetApplyWetness(wetness.Enabled);
|
||||
design._designData.SetIsWet(wetness.ForcedValue);
|
||||
design.SetApplyMeta(MetaIndex.Wetness, wetness.Enabled);
|
||||
|
||||
design.DesignData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
|
||||
PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId, out design.DesignData.IsHuman));
|
||||
if (design.DesignData.ModelId != 0 && forbidNonHuman)
|
||||
design._designData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
|
||||
PrintWarning(customizations.ValidateModelId(design._designData.ModelId, out design._designData.ModelId,
|
||||
out design._designData.IsHuman));
|
||||
if (design._designData.ModelId != 0 && forbidNonHuman)
|
||||
{
|
||||
PrintWarning("Model IDs different from 0 are not currently allowed, reset model id to 0.");
|
||||
design.DesignData.ModelId = 0;
|
||||
design.DesignData.IsHuman = true;
|
||||
design._designData.ModelId = 0;
|
||||
design._designData.IsHuman = true;
|
||||
}
|
||||
else if (!design.DesignData.IsHuman)
|
||||
else if (!design._designData.IsHuman)
|
||||
{
|
||||
var arrayText = json["Array"]?.ToObject<string>() ?? string.Empty;
|
||||
design.DesignData.Customize.LoadBase64(arrayText);
|
||||
design._designData.Customize.LoadBase64(arrayText);
|
||||
design.CustomizeSet = design.SetCustomizationSet(customizations);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -403,51 +676,45 @@ public class DesignBase
|
|||
PrintWarning(customizations.ValidateClan(clan, race, out race, out clan));
|
||||
var gender = (Gender)((json[CustomizeIndex.Gender.ToString()]?["Value"]?.ToObject<byte>() ?? 0) + 1);
|
||||
PrintWarning(customizations.ValidateGender(race, gender, out gender));
|
||||
design.DesignData.Customize.Race = race;
|
||||
design.DesignData.Customize.Clan = clan;
|
||||
design.DesignData.Customize.Gender = gender;
|
||||
design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
|
||||
design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
|
||||
design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
|
||||
|
||||
var set = customizations.AwaitedService.GetList(clan, gender);
|
||||
var bodyType = (CustomizeValue)(json[CustomizeIndex.BodyType.ToString()]?["Value"]?.ToObject<byte>() ?? 1);
|
||||
design._designData.Customize.Race = race;
|
||||
design._designData.Customize.Clan = clan;
|
||||
design._designData.Customize.Gender = gender;
|
||||
design._designData.Customize.BodyType = bodyType;
|
||||
design.CustomizeSet = design.SetCustomizationSet(customizations);
|
||||
design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
|
||||
design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
|
||||
design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
|
||||
design.SetApplyCustomize(CustomizeIndex.BodyType, bodyType != 0);
|
||||
var set = design.CustomizeSet;
|
||||
|
||||
foreach (var idx in CustomizationExtensions.AllBasic)
|
||||
{
|
||||
if (set.IsAvailable(idx))
|
||||
{
|
||||
var tok = json[idx.ToString()];
|
||||
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
|
||||
PrintWarning(CustomizationService.ValidateCustomizeValue(set, design.DesignData.Customize.Face, idx, data, out data,
|
||||
var tok = json[idx.ToString()];
|
||||
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
|
||||
if (set.IsAvailable(idx) && design._designData.Customize.BodyType == 1)
|
||||
PrintWarning(CustomizeService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data,
|
||||
allowUnknown));
|
||||
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
|
||||
design.DesignData.Customize[idx] = data;
|
||||
design.SetApplyCustomize(idx, apply);
|
||||
}
|
||||
else
|
||||
{
|
||||
design.DesignData.Customize[idx] = CustomizeValue.Zero;
|
||||
design.SetApplyCustomize(idx, false);
|
||||
}
|
||||
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
|
||||
design._designData.Customize[idx] = data;
|
||||
design.SetApplyCustomize(idx, apply);
|
||||
}
|
||||
|
||||
design.FixCustomizeApplication(set, design.ApplyCustomize);
|
||||
}
|
||||
|
||||
public void MigrateBase64(ItemManager items, HumanModelList humans, string base64)
|
||||
public void MigrateBase64(CustomizeService customize, ItemManager items, HumanModelList humans, string base64)
|
||||
{
|
||||
try
|
||||
{
|
||||
DesignData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags,
|
||||
out var writeProtected,
|
||||
out var applyHat, out var applyVisor, out var applyWeapon);
|
||||
ApplyEquip = equipFlags;
|
||||
ApplyCustomize = customizeFlags;
|
||||
_designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags,
|
||||
out var writeProtected, out var applyMeta);
|
||||
Application.Equip = equipFlags;
|
||||
ApplyCustomize = customizeFlags;
|
||||
Application.Parameters = 0;
|
||||
Application.Crest = 0;
|
||||
Application.Meta = applyMeta;
|
||||
Application.BonusItem = 0;
|
||||
SetWriteProtected(writeProtected);
|
||||
SetApplyHatVisible(applyHat);
|
||||
SetApplyVisorToggle(applyVisor);
|
||||
SetApplyWeaponVisible(applyWeapon);
|
||||
SetApplyWetness(true);
|
||||
CustomizeSet = SetCustomizationSet(customize);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -455,15 +722,5 @@ public class DesignBase
|
|||
}
|
||||
}
|
||||
|
||||
public void RemoveInvalidCustomize(CustomizationService customizations)
|
||||
{
|
||||
var set = customizations.AwaitedService.GetList(DesignData.Customize.Clan, DesignData.Customize.Gender);
|
||||
foreach (var idx in CustomizationExtensions.AllBasic.Where(i => !set.IsAvailable(i)))
|
||||
{
|
||||
DesignData.Customize[idx] = CustomizeValue.Zero;
|
||||
SetApplyCustomize(idx, false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
using System;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Structs;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Data;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
|
|
@ -16,7 +15,7 @@ public class DesignBase64Migration
|
|||
public const int Base64SizeV4 = 95;
|
||||
|
||||
public static unsafe DesignData MigrateBase64(ItemManager items, HumanModelList humans, string base64, out EquipFlag equipFlags,
|
||||
out CustomizeFlag customizeFlags, out bool writeProtected, out bool applyHat, out bool applyVisor, out bool applyWeapon)
|
||||
out CustomizeFlag customizeFlags, out bool writeProtected, out MetaFlag metaFlags)
|
||||
{
|
||||
static void CheckSize(int length, int requiredLength)
|
||||
{
|
||||
|
|
@ -28,9 +27,7 @@ public class DesignBase64Migration
|
|||
byte applicationFlags;
|
||||
ushort equipFlagsS;
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
applyHat = false;
|
||||
applyVisor = false;
|
||||
applyWeapon = false;
|
||||
metaFlags = MetaFlag.Wetness;
|
||||
var data = new DesignData();
|
||||
switch (bytes[0])
|
||||
{
|
||||
|
|
@ -62,7 +59,7 @@ public class DesignBase64Migration
|
|||
data.SetHatVisible((bytes[90] & 0x01) == 0);
|
||||
data.SetVisor((bytes[90] & 0x10) != 0);
|
||||
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
|
||||
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
|
||||
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
|
||||
break;
|
||||
}
|
||||
case 5:
|
||||
|
|
@ -73,16 +70,19 @@ public class DesignBase64Migration
|
|||
data.SetHatVisible((bytes[90] & 0x01) == 0);
|
||||
data.SetVisor((bytes[90] & 0x10) != 0);
|
||||
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
|
||||
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
|
||||
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
|
||||
break;
|
||||
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
|
||||
}
|
||||
|
||||
customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0;
|
||||
data.SetIsWet((applicationFlags & 0x02) != 0);
|
||||
applyHat = (applicationFlags & 0x04) != 0;
|
||||
applyWeapon = (applicationFlags & 0x08) != 0;
|
||||
applyVisor = (applicationFlags & 0x10) != 0;
|
||||
if ((applicationFlags & 0x04) != 0)
|
||||
metaFlags |= MetaFlag.HatState;
|
||||
if ((applicationFlags & 0x08) != 0)
|
||||
metaFlags |= MetaFlag.WeaponState;
|
||||
if ((applicationFlags & 0x10) != 0)
|
||||
metaFlags |= MetaFlag.VisorState;
|
||||
writeProtected = (applicationFlags & 0x20) != 0;
|
||||
|
||||
equipFlags = 0;
|
||||
|
|
@ -97,16 +97,16 @@ public class DesignBase64Migration
|
|||
|
||||
fixed (byte* ptr = bytes)
|
||||
{
|
||||
var cur = (CharacterWeapon*)(ptr + 30);
|
||||
var eq = (CharacterArmor*)(cur + 2);
|
||||
var cur = (LegacyCharacterWeapon*)(ptr + 30);
|
||||
var eq = (LegacyCharacterArmor*)(cur + 2);
|
||||
|
||||
if (!humans.IsHuman(data.ModelId))
|
||||
{
|
||||
data.LoadNonHuman(data.ModelId, *(Customize*)(ptr + 4), (nint)eq);
|
||||
data.LoadNonHuman(data.ModelId, *(CustomizeArray*)(ptr + 4), (nint)eq);
|
||||
return data;
|
||||
}
|
||||
|
||||
data.Customize.Load(*(Customize*)(ptr + 4));
|
||||
data.Customize = *(CustomizeArray*)(ptr + 4);
|
||||
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
|
||||
{
|
||||
var mdl = eq[idx];
|
||||
|
|
@ -121,9 +121,9 @@ public class DesignBase64Migration
|
|||
data.SetStain(slot, mdl.Stain);
|
||||
}
|
||||
|
||||
var main = cur[0].Set.Id == 0
|
||||
var main = cur[0].Skeleton.Id == 0
|
||||
? items.DefaultSword
|
||||
: items.Identify(EquipSlot.MainHand, cur[0].Set, cur[0].Type, cur[0].Variant);
|
||||
: items.Identify(EquipSlot.MainHand, cur[0].Skeleton, cur[0].Weapon, cur[0].Variant);
|
||||
if (!main.Valid)
|
||||
{
|
||||
Glamourer.Log.Warning("Base64 string invalid, weapon could not be identified.");
|
||||
|
|
@ -135,10 +135,10 @@ public class DesignBase64Migration
|
|||
|
||||
EquipItem off;
|
||||
// Fist weapon hack
|
||||
if (main.ModelId.Id is > 1600 and < 1651 && cur[1].Variant == 0)
|
||||
if (main.PrimaryId.Id is > 1600 and < 1651 && cur[1].Variant == 0)
|
||||
{
|
||||
off = items.Identify(EquipSlot.OffHand, (SetId)(main.ModelId.Id + 50), main.WeaponType, main.Variant, main.Type);
|
||||
var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Set, (Variant)cur[1].Type.Id);
|
||||
off = items.Identify(EquipSlot.OffHand, (PrimaryId)(main.PrimaryId.Id + 50), main.SecondaryId, main.Variant, main.Type);
|
||||
var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Skeleton, (Variant)cur[1].Weapon.Id);
|
||||
if (gauntlet.Valid)
|
||||
{
|
||||
data.SetItem(EquipSlot.Hands, gauntlet);
|
||||
|
|
@ -147,9 +147,9 @@ public class DesignBase64Migration
|
|||
}
|
||||
else
|
||||
{
|
||||
off = cur[0].Set.Id == 0
|
||||
off = cur[0].Skeleton.Id == 0
|
||||
? ItemManager.NothingItem(FullEquipType.Shield)
|
||||
: items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, cur[1].Variant, main.Type);
|
||||
: items.Identify(EquipSlot.OffHand, cur[1].Skeleton, cur[1].Weapon, cur[1].Variant, main.Type);
|
||||
}
|
||||
|
||||
if (main.Type.ValidOffhand() != FullEquipType.Unknown && !off.Valid)
|
||||
|
|
@ -164,16 +164,16 @@ public class DesignBase64Migration
|
|||
}
|
||||
}
|
||||
|
||||
public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags,
|
||||
bool setHat, bool setVisor, bool setWeapon, bool writeProtected, float alpha = 1.0f)
|
||||
public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, MetaFlag meta,
|
||||
bool writeProtected, float alpha = 1.0f)
|
||||
{
|
||||
var data = stackalloc byte[Base64SizeV4];
|
||||
data[0] = 5;
|
||||
data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0)
|
||||
| (save.IsWet() ? 0x02 : 0)
|
||||
| (setHat ? 0x04 : 0)
|
||||
| (setWeapon ? 0x08 : 0)
|
||||
| (setVisor ? 0x10 : 0)
|
||||
| (meta.HasFlag(MetaFlag.HatState) ? 0x04 : 0)
|
||||
| (meta.HasFlag(MetaFlag.WeaponState) ? 0x08 : 0)
|
||||
| (meta.HasFlag(MetaFlag.VisorState) ? 0x10 : 0)
|
||||
| (writeProtected ? 0x20 : 0));
|
||||
data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0)
|
||||
|
|
@ -187,11 +187,13 @@ public class DesignBase64Migration
|
|||
| (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
|
||||
save.Customize.Write((nint)data + 4);
|
||||
((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand));
|
||||
((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand));
|
||||
save.Customize.Write(data + 4);
|
||||
((LegacyCharacterWeapon*)(data + 30))[0] =
|
||||
new LegacyCharacterWeapon(save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand)));
|
||||
((LegacyCharacterWeapon*)(data + 30))[1] =
|
||||
new LegacyCharacterWeapon(save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand)));
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
((CharacterArmor*)(data + 44))[slot.ToIndex()] = save.Item(slot).Armor(save.Stain(slot));
|
||||
((LegacyCharacterArmor*)(data + 44))[slot.ToIndex()] = new LegacyCharacterArmor(save.Item(slot).Armor(save.Stain(slot)));
|
||||
*(ushort*)(data + 84) = 1; // IsSet.
|
||||
*(float*)(data + 86) = 1f;
|
||||
data[90] = (byte)((save.IsHatVisible() ? 0x00 : 0x01)
|
||||
|
|
|
|||
292
Glamourer/Designs/DesignColors.cs
Normal file
292
Glamourer/Designs/DesignColors.cs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Glamourer.Gui;
|
||||
using Glamourer.Services;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public class DesignColorUi(DesignColors colors, Configuration config)
|
||||
{
|
||||
private string _newName = string.Empty;
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var table = ImRaii.Table("designColors", 3, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
var changeString = string.Empty;
|
||||
uint? changeValue = null;
|
||||
|
||||
var buttonSize = new Vector2(ImGui.GetFrameHeight());
|
||||
ImGui.TableSetupColumn("##Delete", ImGuiTableColumnFlags.WidthFixed, buttonSize.X);
|
||||
ImGui.TableSetupColumn("##Select", ImGuiTableColumnFlags.WidthFixed, buttonSize.X);
|
||||
ImGui.TableSetupColumn("Color Name", ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), buttonSize,
|
||||
"Revert the color used for missing design colors to its default.", colors.MissingColor == DesignColors.MissingColorDefault,
|
||||
true))
|
||||
{
|
||||
changeString = DesignColors.MissingColorName;
|
||||
changeValue = DesignColors.MissingColorDefault;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (DrawColorButton(DesignColors.MissingColorName, colors.MissingColor, out var newColor))
|
||||
{
|
||||
changeString = DesignColors.MissingColorName;
|
||||
changeValue = newColor;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
|
||||
ImGui.TextUnformatted(DesignColors.MissingColorName);
|
||||
ImGuiUtil.HoverTooltip("This color is used when the color specified in a design is not available.");
|
||||
|
||||
|
||||
var disabled = !config.DeleteDesignModifier.IsActive();
|
||||
var tt = "Delete this color. This does not remove it from designs using it.";
|
||||
if (disabled)
|
||||
tt += $"\nHold {config.DeleteDesignModifier} to delete.";
|
||||
|
||||
foreach (var ((name, color), idx) in colors.WithIndex())
|
||||
{
|
||||
using var id = ImRaii.PushId(idx);
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, tt, disabled, true))
|
||||
{
|
||||
changeString = name;
|
||||
changeValue = null;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (DrawColorButton(name, color, out newColor))
|
||||
{
|
||||
changeString = name;
|
||||
changeValue = newColor;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
|
||||
ImGui.TextUnformatted(name);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
(tt, disabled) = _newName.Length == 0
|
||||
? ("Specify a name for a new color first.", true)
|
||||
: _newName is DesignColors.MissingColorName or DesignColors.AutomaticName
|
||||
? ($"You can not use the name {DesignColors.MissingColorName} or {DesignColors.AutomaticName}, choose a different one.", true)
|
||||
: colors.ContainsKey(_newName)
|
||||
? ($"The color {_newName} already exists, please choose a different name.", true)
|
||||
: ($"Add a new color {_newName} to your list.", false);
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), buttonSize, tt, disabled, true))
|
||||
{
|
||||
changeString = _newName;
|
||||
changeValue = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
if (ImGui.InputTextWithHint("##newDesignColor", "New Color Name...", ref _newName, 64, ImGuiInputTextFlags.EnterReturnsTrue))
|
||||
{
|
||||
changeString = _newName;
|
||||
changeValue = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
|
||||
if (changeString.Length > 0)
|
||||
{
|
||||
if (!changeValue.HasValue)
|
||||
colors.DeleteColor(changeString);
|
||||
else
|
||||
colors.SetColor(changeString, changeValue.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DrawColorButton(string tooltip, uint color, out uint newColor)
|
||||
{
|
||||
var vec = ImGui.ColorConvertU32ToFloat4(color);
|
||||
if (!ImGui.ColorEdit4(tooltip, ref vec, ImGuiColorEditFlags.AlphaPreviewHalf | ImGuiColorEditFlags.NoInputs))
|
||||
{
|
||||
ImGuiUtil.HoverTooltip(tooltip);
|
||||
newColor = color;
|
||||
return false;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip(tooltip);
|
||||
|
||||
newColor = ImGui.ColorConvertFloat4ToU32(vec);
|
||||
return newColor != color;
|
||||
}
|
||||
}
|
||||
|
||||
public class DesignColors : ISavable, IReadOnlyDictionary<string, uint>
|
||||
{
|
||||
public const string AutomaticName = "Automatic";
|
||||
public const string MissingColorName = "Missing Color";
|
||||
public const uint MissingColorDefault = 0xFF0000D0;
|
||||
|
||||
private readonly SaveService _saveService;
|
||||
private readonly Dictionary<string, uint> _colors = [];
|
||||
public uint MissingColor { get; private set; } = MissingColorDefault;
|
||||
|
||||
public event Action? ColorChanged;
|
||||
|
||||
public DesignColors(SaveService saveService)
|
||||
{
|
||||
_saveService = saveService;
|
||||
Load();
|
||||
}
|
||||
|
||||
public uint GetColor(Design? design)
|
||||
{
|
||||
if (design == null)
|
||||
return ColorId.NormalDesign.Value();
|
||||
|
||||
if (design.Color.Length == 0)
|
||||
return AutoColor(design);
|
||||
|
||||
return TryGetValue(design.Color, out var color) ? color : MissingColor;
|
||||
}
|
||||
|
||||
public void SetColor(string key, uint newColor)
|
||||
{
|
||||
if (key.Length == 0)
|
||||
return;
|
||||
|
||||
if (key is MissingColorName && MissingColor != newColor)
|
||||
{
|
||||
MissingColor = newColor;
|
||||
SaveAndInvoke();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_colors.TryAdd(key, newColor))
|
||||
{
|
||||
SaveAndInvoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_colors.TryGetValue(key, out var color);
|
||||
_colors[key] = newColor;
|
||||
|
||||
if (color != newColor)
|
||||
SaveAndInvoke();
|
||||
}
|
||||
|
||||
private void SaveAndInvoke()
|
||||
{
|
||||
ColorChanged?.Invoke();
|
||||
_saveService.DelaySave(this, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
public void DeleteColor(string key)
|
||||
{
|
||||
if (_colors.Remove(key))
|
||||
SaveAndInvoke();
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.DesignColorFile;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
var jObj = new JObject
|
||||
{
|
||||
["Version"] = 1,
|
||||
["MissingColor"] = MissingColor,
|
||||
["Definitions"] = JToken.FromObject(_colors),
|
||||
};
|
||||
writer.Write(jObj.ToString(Formatting.Indented));
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
_colors.Clear();
|
||||
var file = _saveService.FileNames.DesignColorFile;
|
||||
if (!File.Exists(file))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
var jObj = JObject.Parse(text);
|
||||
var version = jObj["Version"]?.ToObject<int>() ?? 0;
|
||||
switch (version)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
var dict = jObj["Definitions"]?.ToObject<Dictionary<string, uint>>() ?? new Dictionary<string, uint>();
|
||||
_colors.EnsureCapacity(dict.Count);
|
||||
foreach (var kvp in dict)
|
||||
_colors.Add(kvp.Key, kvp.Value);
|
||||
MissingColor = jObj["MissingColor"]?.ToObject<uint>() ?? MissingColorDefault;
|
||||
break;
|
||||
}
|
||||
default: throw new Exception($"Unknown Version {version}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage(ex, "Could not read design color file.", NotificationType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, uint>> GetEnumerator()
|
||||
=> _colors.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> _colors.Count;
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
=> _colors.ContainsKey(key);
|
||||
|
||||
public bool TryGetValue(string key, out uint value)
|
||||
{
|
||||
if (_colors.TryGetValue(key, out value))
|
||||
{
|
||||
if (value == 0)
|
||||
value = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static uint AutoColor(DesignBase design)
|
||||
{
|
||||
var customize = design.ApplyCustomizeExcludingBodyType == 0;
|
||||
var equip = design.Application.Equip == 0;
|
||||
return (customize, equip) switch
|
||||
{
|
||||
(true, true) => ColorId.StateDesign.Value(),
|
||||
(true, false) => ColorId.EquipmentDesign.Value(),
|
||||
(false, true) => ColorId.CustomizationDesign.Value(),
|
||||
(false, false) => ColorId.NormalDesign.Value(),
|
||||
};
|
||||
}
|
||||
|
||||
public uint this[string key]
|
||||
=> _colors[key];
|
||||
|
||||
public IEnumerable<string> Keys
|
||||
=> _colors.Keys;
|
||||
|
||||
public IEnumerable<uint> Values
|
||||
=> _colors.Values;
|
||||
}
|
||||
|
|
@ -1,34 +1,26 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Designs.Links;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.State;
|
||||
using Glamourer.Structs;
|
||||
using Glamourer.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public class DesignConverter
|
||||
public class DesignConverter(
|
||||
SaveService saveService,
|
||||
ItemManager _items,
|
||||
DesignManager _designs,
|
||||
CustomizeService _customize,
|
||||
HumanModelList _humans,
|
||||
DesignLinkLoader _linkLoader)
|
||||
{
|
||||
public const byte Version = 5;
|
||||
|
||||
private readonly ItemManager _items;
|
||||
private readonly DesignManager _designs;
|
||||
private readonly CustomizationService _customize;
|
||||
private readonly HumanModelList _humans;
|
||||
|
||||
public DesignConverter(ItemManager items, DesignManager designs, CustomizationService customize, HumanModelList humans)
|
||||
{
|
||||
_items = items;
|
||||
_designs = designs;
|
||||
_customize = customize;
|
||||
_humans = humans;
|
||||
}
|
||||
public const byte Version = 6;
|
||||
|
||||
public JObject ShareJObject(DesignBase design)
|
||||
=> design.JsonSerialize();
|
||||
|
|
@ -36,40 +28,66 @@ public class DesignConverter
|
|||
public JObject ShareJObject(Design design)
|
||||
=> design.JsonSerialize();
|
||||
|
||||
public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
|
||||
public JObject ShareJObject(ActorState state, in ApplicationRules rules)
|
||||
{
|
||||
var design = Convert(state, equipFlags, customizeFlags);
|
||||
var design = Convert(state, rules);
|
||||
return ShareJObject(design);
|
||||
}
|
||||
|
||||
public string ShareBase64(Design design)
|
||||
=> ShareBackwardCompatible(ShareJObject(design), design);
|
||||
=> ToBase64(ShareJObject(design));
|
||||
|
||||
public string ShareBase64(DesignBase design)
|
||||
=> ShareBackwardCompatible(ShareJObject(design), design);
|
||||
=> ToBase64(ShareJObject(design));
|
||||
|
||||
public string ShareBase64(ActorState state)
|
||||
=> ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All);
|
||||
public string ShareBase64(ActorState state, in ApplicationRules rules)
|
||||
=> ShareBase64(state.ModelData, state.Materials, rules);
|
||||
|
||||
public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
|
||||
public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
|
||||
{
|
||||
var design = Convert(state, equipFlags, customizeFlags);
|
||||
return ShareBackwardCompatible(ShareJObject(design), design);
|
||||
var design = Convert(data, materials, rules);
|
||||
return ToBase64(ShareJObject(design));
|
||||
}
|
||||
|
||||
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
|
||||
public DesignBase Convert(ActorState state, in ApplicationRules rules)
|
||||
=> Convert(state.ModelData, state.Materials, rules);
|
||||
|
||||
public DesignBase Convert(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
|
||||
{
|
||||
var design = _designs.CreateTemporary();
|
||||
design.ApplyEquip = equipFlags & EquipFlagExtensions.All;
|
||||
design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head));
|
||||
design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head));
|
||||
design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand));
|
||||
design.SetApplyWetness(true);
|
||||
design.DesignData = state.ModelData;
|
||||
design.FixCustomizeApplication(_customize, customizeFlags);
|
||||
rules.Apply(design);
|
||||
design.SetDesignData(_customize, data);
|
||||
if (rules.Materials)
|
||||
ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip);
|
||||
return design;
|
||||
}
|
||||
|
||||
public DesignBase? FromJObject(JObject? jObject, bool customize, bool equip)
|
||||
{
|
||||
if (jObject == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var ret = jObject["Identifier"] != null
|
||||
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObject)
|
||||
: DesignBase.LoadDesignBase(_customize, _items, jObject);
|
||||
|
||||
if (!customize)
|
||||
ret.Application.RemoveCustomize();
|
||||
|
||||
if (!equip)
|
||||
ret.Application.RemoveEquip();
|
||||
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Log.Warning($"Failure to parse JObject to design:\n{ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public DesignBase? FromBase64(string base64, bool customize, bool equip, out byte version)
|
||||
{
|
||||
DesignBase ret;
|
||||
|
|
@ -83,14 +101,14 @@ public class DesignConverter
|
|||
case (byte)'{':
|
||||
var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes));
|
||||
ret = jObj1["Identifier"] != null
|
||||
? Design.LoadDesign(_customize, _items, jObj1)
|
||||
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj1)
|
||||
: DesignBase.LoadDesignBase(_customize, _items, jObj1);
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
case 4:
|
||||
ret = _designs.CreateTemporary();
|
||||
ret.MigrateBase64(_items, _humans, base64);
|
||||
ret.MigrateBase64(_customize, _items, _humans, base64);
|
||||
break;
|
||||
case 3:
|
||||
{
|
||||
|
|
@ -98,21 +116,32 @@ public class DesignConverter
|
|||
var jObj2 = JObject.Parse(decompressed);
|
||||
Debug.Assert(version == 3);
|
||||
ret = jObj2["Identifier"] != null
|
||||
? Design.LoadDesign(_customize, _items, jObj2)
|
||||
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
|
||||
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
|
||||
break;
|
||||
}
|
||||
case Version:
|
||||
case 5:
|
||||
{
|
||||
bytes = bytes[DesignBase64Migration.Base64SizeV4..];
|
||||
version = bytes.DecompressToString(out var decompressed);
|
||||
var jObj2 = JObject.Parse(decompressed);
|
||||
Debug.Assert(version == Version);
|
||||
Debug.Assert(version == 5);
|
||||
ret = jObj2["Identifier"] != null
|
||||
? Design.LoadDesign(_customize, _items, jObj2)
|
||||
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
|
||||
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
|
||||
break;
|
||||
}
|
||||
case 6:
|
||||
{
|
||||
version = bytes.DecompressToString(out var decompressed);
|
||||
var jObj2 = JObject.Parse(decompressed);
|
||||
Debug.Assert(version == 6);
|
||||
ret = jObj2["Identifier"] != null
|
||||
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
|
||||
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
|
||||
break;
|
||||
}
|
||||
|
||||
default: throw new Exception($"Unknown Version {bytes[0]}.");
|
||||
}
|
||||
}
|
||||
|
|
@ -123,43 +152,103 @@ public class DesignConverter
|
|||
}
|
||||
|
||||
if (!customize)
|
||||
{
|
||||
ret.ApplyCustomize = 0;
|
||||
ret.SetApplyWetness(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret.FixCustomizeApplication(_customize, ret.ApplyCustomize);
|
||||
}
|
||||
ret.Application.RemoveCustomize();
|
||||
|
||||
if (!equip)
|
||||
{
|
||||
ret.ApplyEquip = 0;
|
||||
ret.SetApplyHatVisible(false);
|
||||
ret.SetApplyWeaponVisible(false);
|
||||
ret.SetApplyVisorToggle(false);
|
||||
}
|
||||
ret.Application.RemoveEquip();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static string ShareBase64(JObject jObj)
|
||||
public static string ToBase64(JToken jObject)
|
||||
{
|
||||
var json = jObj.ToString(Formatting.None);
|
||||
var json = jObject.ToString(Formatting.None);
|
||||
var compressed = json.Compress(Version);
|
||||
return System.Convert.ToBase64String(compressed);
|
||||
}
|
||||
|
||||
private static string ShareBackwardCompatible(JObject jObject, DesignBase design)
|
||||
public IEnumerable<(EquipSlot Slot, EquipItem Item, StainIds Stains)> FromDrawData(IReadOnlyList<CharacterArmor> armors,
|
||||
CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings)
|
||||
{
|
||||
var oldBase64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomize,
|
||||
design.DoApplyHatVisible(), design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected(), 1f);
|
||||
var oldBytes = System.Convert.FromBase64String(oldBase64);
|
||||
var json = jObject.ToString(Formatting.None);
|
||||
var compressed = json.Compress(Version);
|
||||
var bytes = new byte[oldBytes.Length + compressed.Length];
|
||||
oldBytes.CopyTo(bytes, 0);
|
||||
compressed.CopyTo(bytes, oldBytes.Length);
|
||||
return System.Convert.ToBase64String(bytes);
|
||||
if (armors.Count != 10)
|
||||
throw new ArgumentException("Invalid length of armor array.");
|
||||
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
var index = (int)slot.ToIndex();
|
||||
var armor = armors[index];
|
||||
var item = _items.Identify(slot, armor.Set, armor.Variant);
|
||||
if (!item.Valid)
|
||||
{
|
||||
if (!skipWarnings)
|
||||
Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified.");
|
||||
item = ItemManager.NothingItem(slot);
|
||||
}
|
||||
|
||||
yield return (slot, item, armor.Stains);
|
||||
}
|
||||
|
||||
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant);
|
||||
if (!skipWarnings && !mh.Valid)
|
||||
{
|
||||
Glamourer.Log.Warning($"Appearance data {mainhand} for mainhand weapon invalid, item could not be identified.");
|
||||
mh = _items.DefaultSword;
|
||||
}
|
||||
|
||||
yield return (EquipSlot.MainHand, mh, mainhand.Stains);
|
||||
|
||||
var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type);
|
||||
if (!skipWarnings && !oh.Valid)
|
||||
{
|
||||
Glamourer.Log.Warning($"Appearance data {offhand} for offhand weapon invalid, item could not be identified.");
|
||||
oh = _items.GetDefaultOffhand(mh);
|
||||
if (!oh.Valid)
|
||||
oh = ItemManager.NothingItem(FullEquipType.Shield);
|
||||
}
|
||||
|
||||
yield return (EquipSlot.OffHand, oh, offhand.Stains);
|
||||
}
|
||||
|
||||
private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials,
|
||||
EquipFlag equipFlags = EquipFlagExtensions.All, BonusItemFlag bonusFlags = BonusExtensions.All)
|
||||
{
|
||||
foreach (var (key, value) in materials.Values)
|
||||
{
|
||||
var idx = MaterialValueIndex.FromKey(key);
|
||||
if (idx.RowIndex >= ColorTable.NumRows)
|
||||
continue;
|
||||
if (idx.MaterialIndex >= MaterialService.MaterialsPerModel)
|
||||
continue;
|
||||
|
||||
switch (idx.DrawObject)
|
||||
{
|
||||
case MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0:
|
||||
if ((equipFlags & (EquipFlag.Mainhand | EquipFlag.MainhandStain)) == 0)
|
||||
continue;
|
||||
|
||||
break;
|
||||
case MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0:
|
||||
if ((equipFlags & (EquipFlag.Offhand | EquipFlag.OffhandStain)) == 0)
|
||||
continue;
|
||||
|
||||
break;
|
||||
case MaterialValueIndex.DrawObjectType.Human:
|
||||
if (idx.SlotIndex < 10)
|
||||
{
|
||||
if ((((uint)idx.SlotIndex).ToEquipSlot().ToBothFlags() & equipFlags) == 0)
|
||||
continue;
|
||||
}
|
||||
else if (idx.SlotIndex >= 16)
|
||||
{
|
||||
if (((idx.SlotIndex - 16u).ToBonusSlot() & bonusFlags) == 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
manager.AddOrUpdateValue(idx, value.Convert());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +1,159 @@
|
|||
using System;
|
||||
using System.Buffers.Text;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.Services;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.String.Functions;
|
||||
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public unsafe struct DesignData
|
||||
{
|
||||
private string _nameHead = string.Empty;
|
||||
private string _nameBody = string.Empty;
|
||||
private string _nameHands = string.Empty;
|
||||
private string _nameLegs = string.Empty;
|
||||
private string _nameFeet = string.Empty;
|
||||
private string _nameEars = string.Empty;
|
||||
private string _nameNeck = string.Empty;
|
||||
private string _nameWrists = string.Empty;
|
||||
private string _nameRFinger = string.Empty;
|
||||
private string _nameLFinger = string.Empty;
|
||||
private string _nameMainhand = string.Empty;
|
||||
private string _nameOffhand = string.Empty;
|
||||
private fixed uint _itemIds[12];
|
||||
private fixed ushort _iconIds[12];
|
||||
private fixed byte _equipmentBytes[48];
|
||||
public Customize Customize = Customize.Default;
|
||||
public uint ModelId;
|
||||
private WeaponType _secondaryMainhand;
|
||||
private WeaponType _secondaryOffhand;
|
||||
private FullEquipType _typeMainhand;
|
||||
private FullEquipType _typeOffhand;
|
||||
private byte _states;
|
||||
public bool IsHuman = true;
|
||||
public const int NumEquipment = 10;
|
||||
public const int EquipmentByteSize = NumEquipment * CharacterArmor.Size;
|
||||
public const int NumBonusItems = 1;
|
||||
public const int NumWeapons = 2;
|
||||
|
||||
private string _nameHead = string.Empty;
|
||||
private string _nameBody = string.Empty;
|
||||
private string _nameHands = string.Empty;
|
||||
private string _nameLegs = string.Empty;
|
||||
private string _nameFeet = string.Empty;
|
||||
private string _nameEars = string.Empty;
|
||||
private string _nameNeck = string.Empty;
|
||||
private string _nameWrists = string.Empty;
|
||||
private string _nameRFinger = string.Empty;
|
||||
private string _nameLFinger = string.Empty;
|
||||
private string _nameMainhand = string.Empty;
|
||||
private string _nameOffhand = string.Empty;
|
||||
private string _nameGlasses = string.Empty;
|
||||
|
||||
private fixed uint _itemIds[NumEquipment + NumWeapons];
|
||||
private fixed uint _iconIds[NumEquipment + NumWeapons + NumBonusItems];
|
||||
private fixed byte _equipmentBytes[EquipmentByteSize + NumWeapons * CharacterWeapon.Size];
|
||||
private fixed ushort _bonusIds[NumBonusItems];
|
||||
private fixed ushort _bonusModelIds[NumBonusItems];
|
||||
private fixed byte _bonusVariants[NumBonusItems];
|
||||
public CustomizeParameterData Parameters;
|
||||
public CustomizeArray Customize = CustomizeArray.Default;
|
||||
public uint ModelId;
|
||||
public CrestFlag CrestVisibility;
|
||||
private FullEquipType _typeMainhand;
|
||||
private FullEquipType _typeOffhand;
|
||||
private byte _states;
|
||||
public bool IsHuman = true;
|
||||
|
||||
public DesignData()
|
||||
{ }
|
||||
|
||||
public readonly StainId Stain(EquipSlot slot)
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool ContainsName(LowerString name)
|
||||
=> ItemNames.Any(name.IsContained);
|
||||
|
||||
public readonly StainIds Stain(EquipSlot slot)
|
||||
{
|
||||
var index = slot.ToIndex();
|
||||
return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3];
|
||||
return index switch
|
||||
{
|
||||
< 10 => new StainIds(_equipmentBytes[CharacterArmor.Size * index + 3], _equipmentBytes[CharacterArmor.Size * index + 4]),
|
||||
10 => new StainIds(_equipmentBytes[EquipmentByteSize + 6], _equipmentBytes[EquipmentByteSize + 7]),
|
||||
11 => new StainIds(_equipmentBytes[EquipmentByteSize + 14], _equipmentBytes[EquipmentByteSize + 15]),
|
||||
_ => StainIds.None,
|
||||
};
|
||||
}
|
||||
|
||||
public FullEquipType MainhandType
|
||||
public readonly bool Crest(CrestFlag slot)
|
||||
=> CrestVisibility.HasFlag(slot);
|
||||
|
||||
public readonly IEnumerable<string> ItemNames
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
yield return _nameHead;
|
||||
yield return _nameBody;
|
||||
yield return _nameHands;
|
||||
yield return _nameLegs;
|
||||
yield return _nameFeet;
|
||||
yield return _nameEars;
|
||||
yield return _nameNeck;
|
||||
yield return _nameWrists;
|
||||
yield return _nameRFinger;
|
||||
yield return _nameLFinger;
|
||||
yield return _nameMainhand;
|
||||
yield return _nameOffhand;
|
||||
yield return _nameGlasses;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
public readonly IEnumerable<string> FilteredItemNames(EquipFlag item, BonusItemFlag bonusItem)
|
||||
{
|
||||
if (item.HasFlag(EquipFlag.Head))
|
||||
yield return _nameHead;
|
||||
if (item.HasFlag(EquipFlag.Body))
|
||||
yield return _nameBody;
|
||||
if (item.HasFlag(EquipFlag.Hands))
|
||||
yield return _nameHands;
|
||||
if (item.HasFlag(EquipFlag.Legs))
|
||||
yield return _nameLegs;
|
||||
if (item.HasFlag(EquipFlag.Feet))
|
||||
yield return _nameFeet;
|
||||
if (item.HasFlag(EquipFlag.Ears))
|
||||
yield return _nameEars;
|
||||
if (item.HasFlag(EquipFlag.Neck))
|
||||
yield return _nameNeck;
|
||||
if (item.HasFlag(EquipFlag.Wrist))
|
||||
yield return _nameWrists;
|
||||
if (item.HasFlag(EquipFlag.RFinger))
|
||||
yield return _nameRFinger;
|
||||
if (item.HasFlag(EquipFlag.LFinger))
|
||||
yield return _nameLFinger;
|
||||
if (item.HasFlag(EquipFlag.Mainhand))
|
||||
yield return _nameMainhand;
|
||||
if (item.HasFlag(EquipFlag.Offhand))
|
||||
yield return _nameOffhand;
|
||||
if (bonusItem.HasFlag(BonusItemFlag.Glasses))
|
||||
yield return _nameGlasses;
|
||||
}
|
||||
|
||||
public readonly FullEquipType MainhandType
|
||||
=> _typeMainhand;
|
||||
|
||||
public FullEquipType OffhandType
|
||||
public readonly FullEquipType OffhandType
|
||||
=> _typeOffhand;
|
||||
|
||||
public readonly EquipItem Item(EquipSlot slot)
|
||||
=> slot.ToIndex() switch
|
||||
{
|
||||
fixed (byte* ptr = _equipmentBytes)
|
||||
{
|
||||
return slot.ToIndex() switch
|
||||
{
|
||||
// @formatter:off
|
||||
0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], ((CharacterArmor*)ptr)[0].Set, 0, ((CharacterArmor*)ptr)[0].Variant, FullEquipType.Head, name: _nameHead ),
|
||||
1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], ((CharacterArmor*)ptr)[1].Set, 0, ((CharacterArmor*)ptr)[1].Variant, FullEquipType.Body, name: _nameBody ),
|
||||
2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], ((CharacterArmor*)ptr)[2].Set, 0, ((CharacterArmor*)ptr)[2].Variant, FullEquipType.Hands, name: _nameHands ),
|
||||
3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], ((CharacterArmor*)ptr)[3].Set, 0, ((CharacterArmor*)ptr)[3].Variant, FullEquipType.Legs, name: _nameLegs ),
|
||||
4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], ((CharacterArmor*)ptr)[4].Set, 0, ((CharacterArmor*)ptr)[4].Variant, FullEquipType.Feet, name: _nameFeet ),
|
||||
5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], ((CharacterArmor*)ptr)[5].Set, 0, ((CharacterArmor*)ptr)[5].Variant, FullEquipType.Ears, name: _nameEars ),
|
||||
6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], ((CharacterArmor*)ptr)[6].Set, 0, ((CharacterArmor*)ptr)[6].Variant, FullEquipType.Neck, name: _nameNeck ),
|
||||
7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], ((CharacterArmor*)ptr)[7].Set, 0, ((CharacterArmor*)ptr)[7].Variant, FullEquipType.Wrists, name: _nameWrists ),
|
||||
8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], ((CharacterArmor*)ptr)[8].Set, 0, ((CharacterArmor*)ptr)[8].Variant, FullEquipType.Finger, name: _nameRFinger ),
|
||||
9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], ((CharacterArmor*)ptr)[9].Set, 0, ((CharacterArmor*)ptr)[9].Variant, FullEquipType.Finger, name: _nameLFinger ),
|
||||
10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], *(PrimaryId*)(ptr + EquipmentByteSize + 0), *(SecondaryId*)(ptr + EquipmentByteSize + 2), *(Variant*)(ptr + EquipmentByteSize + 4), _typeMainhand, name: _nameMainhand),
|
||||
11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], *(PrimaryId*)(ptr + EquipmentByteSize + 8), *(SecondaryId*)(ptr + EquipmentByteSize + 10), *(Variant*)(ptr + EquipmentByteSize + 12), _typeOffhand, name: _nameOffhand ),
|
||||
_ => new EquipItem(),
|
||||
// @formatter:on
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public readonly EquipItem BonusItem(BonusItemFlag slot)
|
||||
=> slot switch
|
||||
{
|
||||
// @formatter:off
|
||||
0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], (SetId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (WeaponType)0, _equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ),
|
||||
1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], (SetId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (WeaponType)0, _equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ),
|
||||
2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], (SetId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (WeaponType)0, _equipmentBytes[10], FullEquipType.Hands, name: _nameHands ),
|
||||
3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], (SetId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (WeaponType)0, _equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ),
|
||||
4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], (SetId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (WeaponType)0, _equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ),
|
||||
5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], (SetId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (WeaponType)0, _equipmentBytes[22], FullEquipType.Ears, name: _nameEars ),
|
||||
6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], (SetId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (WeaponType)0, _equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ),
|
||||
7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], (SetId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (WeaponType)0, _equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ),
|
||||
8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], (SetId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (WeaponType)0, _equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ),
|
||||
9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], (SetId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (WeaponType)0, _equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ),
|
||||
10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], (SetId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, _equipmentBytes[42], _typeMainhand, name: _nameMainhand),
|
||||
11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], (SetId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, _equipmentBytes[46], _typeOffhand, name: _nameOffhand ),
|
||||
_ => new EquipItem(),
|
||||
BonusItemFlag.Glasses => EquipItem.FromBonusIds(_bonusIds[0], _iconIds[12], _bonusModelIds[0], _bonusVariants[0], BonusItemFlag.Glasses, _nameGlasses),
|
||||
_ => EquipItem.BonusItemNothing(slot),
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
|
|
@ -96,22 +182,22 @@ public unsafe struct DesignData
|
|||
{
|
||||
fixed (byte* ptr = _equipmentBytes)
|
||||
{
|
||||
var armorPtr = (CharacterArmor*)ptr;
|
||||
return slot is EquipSlot.MainHand ? armorPtr[10].ToWeapon(_secondaryMainhand) : armorPtr[11].ToWeapon(_secondaryOffhand);
|
||||
var weaponPtr = (CharacterWeapon*)(ptr + EquipmentByteSize);
|
||||
return weaponPtr[slot is EquipSlot.MainHand ? 0 : 1];
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetItem(EquipSlot slot, EquipItem item)
|
||||
{
|
||||
var index = slot.ToIndex();
|
||||
if (index > 11)
|
||||
if (index > NumEquipment + NumWeapons)
|
||||
return false;
|
||||
|
||||
_itemIds[index] = item.ItemId.Id;
|
||||
_iconIds[index] = item.IconId.Id;
|
||||
_equipmentBytes[4 * index + 0] = (byte)item.ModelId.Id;
|
||||
_equipmentBytes[4 * index + 1] = (byte)(item.ModelId.Id >> 8);
|
||||
_equipmentBytes[4 * index + 2] = item.Variant.Id;
|
||||
_itemIds[index] = item.ItemId.Id;
|
||||
_iconIds[index] = item.IconId.Id;
|
||||
_equipmentBytes[CharacterArmor.Size * index + 0] = (byte)item.PrimaryId.Id;
|
||||
_equipmentBytes[CharacterArmor.Size * index + 1] = (byte)(item.PrimaryId.Id >> 8);
|
||||
_equipmentBytes[CharacterArmor.Size * index + 2] = item.Variant.Id;
|
||||
switch (index)
|
||||
{
|
||||
// @formatter:off
|
||||
|
|
@ -127,36 +213,93 @@ public unsafe struct DesignData
|
|||
case 9: _nameLFinger = item.Name; return true;
|
||||
// @formatter:on
|
||||
case 10:
|
||||
_nameMainhand = item.Name;
|
||||
_secondaryMainhand = item.WeaponType;
|
||||
_typeMainhand = item.Type;
|
||||
_nameMainhand = item.Name;
|
||||
_equipmentBytes[EquipmentByteSize + 2] = (byte)item.SecondaryId.Id;
|
||||
_equipmentBytes[EquipmentByteSize + 3] = (byte)(item.SecondaryId.Id >> 8);
|
||||
_equipmentBytes[EquipmentByteSize + 4] = item.Variant.Id;
|
||||
_typeMainhand = item.Type;
|
||||
return true;
|
||||
case 11:
|
||||
_nameOffhand = item.Name;
|
||||
_secondaryOffhand = item.WeaponType;
|
||||
_typeOffhand = item.Type;
|
||||
_nameOffhand = item.Name;
|
||||
_equipmentBytes[EquipmentByteSize + 10] = (byte)item.SecondaryId.Id;
|
||||
_equipmentBytes[EquipmentByteSize + 11] = (byte)(item.SecondaryId.Id >> 8);
|
||||
_equipmentBytes[EquipmentByteSize + 12] = item.Variant.Id;
|
||||
_typeOffhand = item.Type;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetStain(EquipSlot slot, StainId stain)
|
||||
public bool SetBonusItem(BonusItemFlag slot, EquipItem item)
|
||||
{
|
||||
var index = slot.ToIndex();
|
||||
if (index > NumBonusItems)
|
||||
return false;
|
||||
|
||||
_iconIds[NumEquipment + NumWeapons + index] = item.IconId.Id;
|
||||
_bonusIds[index] = item.Id.BonusItem.Id;
|
||||
_bonusModelIds[index] = item.PrimaryId.Id;
|
||||
_bonusVariants[index] = item.Variant.Id;
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
_nameGlasses = item.Name;
|
||||
return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetStain(EquipSlot slot, StainIds stains)
|
||||
=> slot.ToIndex() switch
|
||||
{
|
||||
0 => SetIfDifferent(ref _equipmentBytes[3], stain.Id),
|
||||
1 => SetIfDifferent(ref _equipmentBytes[7], stain.Id),
|
||||
2 => SetIfDifferent(ref _equipmentBytes[11], stain.Id),
|
||||
3 => SetIfDifferent(ref _equipmentBytes[15], stain.Id),
|
||||
4 => SetIfDifferent(ref _equipmentBytes[19], stain.Id),
|
||||
5 => SetIfDifferent(ref _equipmentBytes[23], stain.Id),
|
||||
6 => SetIfDifferent(ref _equipmentBytes[27], stain.Id),
|
||||
7 => SetIfDifferent(ref _equipmentBytes[31], stain.Id),
|
||||
8 => SetIfDifferent(ref _equipmentBytes[35], stain.Id),
|
||||
9 => SetIfDifferent(ref _equipmentBytes[39], stain.Id),
|
||||
10 => SetIfDifferent(ref _equipmentBytes[43], stain.Id),
|
||||
11 => SetIfDifferent(ref _equipmentBytes[47], stain.Id),
|
||||
_ => false,
|
||||
// @formatter:off
|
||||
0 => SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
1 => SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
2 => SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
3 => SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
4 => SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
5 => SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
6 => SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
7 => SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
8 => SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
9 => SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 4], stains.Stain2.Id),
|
||||
10 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 6], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 7], stains.Stain2.Id),
|
||||
11 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 14], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 15], stains.Stain2.Id),
|
||||
_ => false,
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
public bool SetCrest(CrestFlag slot, bool visible)
|
||||
{
|
||||
var newValue = visible ? CrestVisibility | slot : CrestVisibility & ~slot;
|
||||
if (newValue == CrestVisibility)
|
||||
return false;
|
||||
|
||||
CrestVisibility = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly bool GetMeta(MetaIndex index)
|
||||
=> index switch
|
||||
{
|
||||
MetaIndex.Wetness => IsWet(),
|
||||
MetaIndex.HatState => IsHatVisible(),
|
||||
MetaIndex.VisorState => IsVisorToggled(),
|
||||
MetaIndex.WeaponState => IsWeaponVisible(),
|
||||
MetaIndex.EarState => AreEarsVisible(),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public bool SetMeta(MetaIndex index, bool value)
|
||||
=> index switch
|
||||
{
|
||||
MetaIndex.Wetness => SetIsWet(value),
|
||||
MetaIndex.HatState => SetHatVisible(value),
|
||||
MetaIndex.VisorState => SetVisor(value),
|
||||
MetaIndex.WeaponState => SetWeaponVisible(value),
|
||||
MetaIndex.EarState => SetEarsVisible(value),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public readonly bool IsWet()
|
||||
|
|
@ -199,6 +342,9 @@ public unsafe struct DesignData
|
|||
public readonly bool IsWeaponVisible()
|
||||
=> (_states & 0x08) == 0x08;
|
||||
|
||||
public readonly bool AreEarsVisible()
|
||||
=> (_states & 0x10) == 0x00;
|
||||
|
||||
public bool SetWeaponVisible(bool value)
|
||||
{
|
||||
if (value == IsWeaponVisible())
|
||||
|
|
@ -208,26 +354,45 @@ public unsafe struct DesignData
|
|||
return true;
|
||||
}
|
||||
|
||||
public bool SetEarsVisible(bool value)
|
||||
{
|
||||
if (value == AreEarsVisible())
|
||||
return false;
|
||||
|
||||
_states = (byte)(value ? _states & ~0x10 : _states | 0x10);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetDefaultEquipment(ItemManager items)
|
||||
{
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
SetItem(slot, ItemManager.NothingItem(slot));
|
||||
SetStain(slot, 0);
|
||||
SetStain(slot, StainIds.None);
|
||||
SetCrest(slot.ToCrestFlag(), false);
|
||||
}
|
||||
|
||||
SetItem(EquipSlot.MainHand, items.DefaultSword);
|
||||
SetStain(EquipSlot.MainHand, 0);
|
||||
SetStain(EquipSlot.MainHand, StainIds.None);
|
||||
SetCrest(CrestFlag.MainHand, false);
|
||||
SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield));
|
||||
SetStain(EquipSlot.OffHand, 0);
|
||||
SetStain(EquipSlot.OffHand, StainIds.None);
|
||||
SetCrest(CrestFlag.OffHand, false);
|
||||
SetDefaultBonusItems();
|
||||
}
|
||||
|
||||
public void SetDefaultBonusItems()
|
||||
{
|
||||
foreach (var slot in BonusExtensions.AllFlags)
|
||||
SetBonusItem(slot, EquipItem.BonusItemNothing(slot));
|
||||
}
|
||||
|
||||
|
||||
public bool LoadNonHuman(uint modelId, Customize customize, nint equipData)
|
||||
public bool LoadNonHuman(uint modelId, CustomizeArray customize, nint equipData)
|
||||
{
|
||||
ModelId = modelId;
|
||||
IsHuman = false;
|
||||
Customize.Load(customize);
|
||||
Customize.Read(customize.Data);
|
||||
fixed (byte* ptr = _equipmentBytes)
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked(ptr, (byte*)equipData, 40);
|
||||
|
|
@ -235,13 +400,14 @@ public unsafe struct DesignData
|
|||
|
||||
SetHatVisible(true);
|
||||
SetWeaponVisible(true);
|
||||
SetEarsVisible(true);
|
||||
SetVisor(false);
|
||||
fixed (uint* ptr = _itemIds)
|
||||
{
|
||||
MemoryUtility.MemSet(ptr, 0, 10 * 4);
|
||||
}
|
||||
|
||||
fixed (ushort* ptr = _iconIds)
|
||||
fixed (uint* ptr = _iconIds)
|
||||
{
|
||||
MemoryUtility.MemSet(ptr, 0, 10 * 2);
|
||||
}
|
||||
|
|
@ -256,13 +422,14 @@ public unsafe struct DesignData
|
|||
_nameWrists = string.Empty;
|
||||
_nameRFinger = string.Empty;
|
||||
_nameLFinger = string.Empty;
|
||||
_nameGlasses = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly byte[] GetCustomizeBytes()
|
||||
{
|
||||
var ret = new byte[CustomizeData.Size];
|
||||
fixed (byte* retPtr = ret, inPtr = Customize.Data.Data)
|
||||
var ret = new byte[CustomizeArray.Size];
|
||||
fixed (byte* retPtr = ret, inPtr = Customize.Data)
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length);
|
||||
}
|
||||
|
|
@ -272,7 +439,7 @@ public unsafe struct DesignData
|
|||
|
||||
public readonly byte[] GetEquipmentBytes()
|
||||
{
|
||||
var ret = new byte[40];
|
||||
var ret = new byte[80];
|
||||
fixed (byte* retPtr = ret, inPtr = _equipmentBytes)
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length);
|
||||
|
|
@ -293,8 +460,8 @@ public unsafe struct DesignData
|
|||
{
|
||||
fixed (byte* dataPtr = _equipmentBytes)
|
||||
{
|
||||
var data = new Span<byte>(dataPtr, 40);
|
||||
return Convert.TryFromBase64String(base64, data, out var written) && written == 40;
|
||||
var data = new Span<byte>(dataPtr, 80);
|
||||
return Convert.TryFromBase64String(base64, data, out var written) && written == 80;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
397
Glamourer/Designs/DesignEditor.cs
Normal file
397
Glamourer/Designs/DesignEditor.cs
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
using Glamourer.Designs.History;
|
||||
using Glamourer.Designs.Links;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.Services;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public class DesignEditor(
|
||||
SaveService saveService,
|
||||
DesignChanged designChanged,
|
||||
CustomizeService customizations,
|
||||
ItemManager items,
|
||||
Configuration config)
|
||||
: IDesignEditor
|
||||
{
|
||||
protected readonly DesignChanged DesignChanged = designChanged;
|
||||
protected readonly SaveService SaveService = saveService;
|
||||
protected readonly ItemManager Items = items;
|
||||
protected readonly CustomizeService Customizations = customizations;
|
||||
protected readonly Configuration Config = config;
|
||||
protected readonly Dictionary<Guid, DesignData> UndoStore = [];
|
||||
|
||||
private bool _forceFullItemOff;
|
||||
|
||||
/// <summary> Whether an Undo for the given design is possible. </summary>
|
||||
public bool CanUndo(Design? design)
|
||||
=> design != null && UndoStore.ContainsKey(design.Identifier);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
var oldValue = design.DesignData.Customize[idx];
|
||||
switch (idx)
|
||||
{
|
||||
case CustomizeIndex.Race:
|
||||
case CustomizeIndex.BodyType:
|
||||
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
|
||||
return;
|
||||
case CustomizeIndex.Clan:
|
||||
{
|
||||
var customize = design.DesignData.Customize;
|
||||
if (Customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0)
|
||||
return;
|
||||
if (!design.SetCustomize(Customizations, customize))
|
||||
return;
|
||||
|
||||
break;
|
||||
}
|
||||
case CustomizeIndex.Gender:
|
||||
{
|
||||
var customize = design.DesignData.Customize;
|
||||
if (Customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0)
|
||||
return;
|
||||
if (!design.SetCustomize(Customizations, customize))
|
||||
return;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (!Customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender,
|
||||
design.DesignData.Customize.Face, idx, value)
|
||||
|| !design.GetDesignDataRef().Customize.Set(idx, value))
|
||||
return;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
|
||||
SaveService.QueueSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Customize, design, new CustomizeTransaction(idx, oldValue, value));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeEntireCustomize(object data, in CustomizeArray customize, CustomizeFlag apply, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
var (newCustomize, applied, changed) = Customizations.Combine(design.DesignData.Customize, customize, apply, true);
|
||||
if (changed == 0)
|
||||
return;
|
||||
|
||||
var oldCustomize = design.DesignData.Customize;
|
||||
design.SetCustomize(Customizations, newCustomize);
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}.");
|
||||
SaveService.QueueSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, new EntireCustomizeTransaction(changed, oldCustomize, newCustomize));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
var old = design.DesignData.Parameters[flag];
|
||||
if (!design.GetDesignDataRef().Parameters.Set(flag, value))
|
||||
return;
|
||||
|
||||
var @new = design.DesignData.Parameters[flag];
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}.");
|
||||
SaveService.QueueSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Parameter, design, new ParameterTransaction(flag, old, @new));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
switch (slot)
|
||||
{
|
||||
case EquipSlot.MainHand:
|
||||
{
|
||||
var currentMain = design.DesignData.Item(EquipSlot.MainHand);
|
||||
var currentOff = design.DesignData.Item(EquipSlot.OffHand);
|
||||
if (!Items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item))
|
||||
return;
|
||||
|
||||
if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets))
|
||||
return;
|
||||
|
||||
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug(
|
||||
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId}).");
|
||||
DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
|
||||
new WeaponTransaction(currentMain, currentOff, currentGauntlets, item, newOff ?? currentOff,
|
||||
newGauntlets ?? currentGauntlets));
|
||||
return;
|
||||
}
|
||||
case EquipSlot.OffHand:
|
||||
{
|
||||
var currentMain = design.DesignData.Item(EquipSlot.MainHand);
|
||||
var currentOff = design.DesignData.Item(EquipSlot.OffHand);
|
||||
if (!Items.IsOffhandValid(currentOff.Type, item.ItemId, out item))
|
||||
return;
|
||||
|
||||
if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item))
|
||||
return;
|
||||
|
||||
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug(
|
||||
$"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId}).");
|
||||
DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
|
||||
new WeaponTransaction(currentMain, currentOff, currentGauntlets, currentMain, item, currentGauntlets));
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (!Items.IsItemValid(slot, item.Id, out item))
|
||||
return;
|
||||
|
||||
var old = design.DesignData.Item(slot);
|
||||
if (!design.GetDesignDataRef().SetItem(slot, item))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
Glamourer.Log.Debug(
|
||||
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}).");
|
||||
SaveService.QueueSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Equip, design, new EquipTransaction(slot, old, item));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
if (item.Type.ToBonus() != slot)
|
||||
return;
|
||||
|
||||
var oldItem = design.DesignData.BonusItem(slot);
|
||||
if (!design.GetDesignDataRef().SetBonusItem(slot, item))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set {slot} bonus item to {item}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, new BonusItemTransaction(slot, oldItem, item));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
if (Items.ValidateStain(stains, out var _, false).Length > 0)
|
||||
return;
|
||||
|
||||
var oldStain = design.DesignData.Stain(slot);
|
||||
if (!design.GetDesignDataRef().SetStain(slot, stains))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stains}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.Stains, design, new StainTransaction(slot, oldStain, stains));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings _ = default)
|
||||
{
|
||||
if (item.HasValue)
|
||||
ChangeItem(data, slot, item.Value, _);
|
||||
if (stains.HasValue)
|
||||
ChangeStains(data, slot, stains.Value, _);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
var oldCrest = design.DesignData.Crest(slot);
|
||||
if (!design.GetDesignDataRef().SetCrest(slot, crest))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.Crest, design, new CrestTransaction(slot, oldCrest, crest));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ChangeMetaState(object data, MetaIndex metaIndex, bool value, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
if (!design.GetDesignDataRef().SetMeta(metaIndex, value))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set value of {metaIndex} to {value}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.Other, design, new MetaTransaction(metaIndex, !value, value));
|
||||
}
|
||||
|
||||
public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert)
|
||||
{
|
||||
var materials = design.GetMaterialDataRef();
|
||||
if (!materials.TryGetValue(index, out var oldValue))
|
||||
return;
|
||||
|
||||
materials.AddOrUpdateValue(index, oldValue with { Revert = revert });
|
||||
Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}");
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert));
|
||||
}
|
||||
|
||||
public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row)
|
||||
{
|
||||
var materials = design.GetMaterialDataRef();
|
||||
if (materials.TryGetValue(index, out var oldValue))
|
||||
{
|
||||
if (!row.HasValue)
|
||||
{
|
||||
materials.RemoveValue(index);
|
||||
Glamourer.Log.Debug($"Removed advanced dye value for {index}.");
|
||||
}
|
||||
else if (!row.Value.NearEqual(oldValue.Value))
|
||||
{
|
||||
materials.UpdateValue(index, new MaterialValueDesign(row.Value, oldValue.Enabled, oldValue.Revert), out _);
|
||||
Glamourer.Log.Debug($"Updated advanced dye value for {index} to new value.");
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!row.HasValue)
|
||||
return;
|
||||
if (!materials.TryAddValue(index, new MaterialValueDesign(row.Value, true, false)))
|
||||
return;
|
||||
|
||||
Glamourer.Log.Debug($"Added new advanced dye value for {index}.");
|
||||
}
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.DelaySave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Material, design, new MaterialTransaction(index, oldValue.Value, row));
|
||||
}
|
||||
|
||||
public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value)
|
||||
{
|
||||
var materials = design.GetMaterialDataRef();
|
||||
if (!materials.TryGetValue(index, out var oldValue) || oldValue.Enabled == value)
|
||||
return;
|
||||
|
||||
materials.AddOrUpdateValue(index, oldValue with { Enabled = value });
|
||||
Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}.");
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, new ApplicationTransaction(index, !value, value));
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ApplyDesign(object data, MergedDesign other, ApplySettings settings = default)
|
||||
=> ApplyDesign(data, other.Design, settings);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default)
|
||||
{
|
||||
var design = (Design)data;
|
||||
UndoStore[design.Identifier] = design.DesignData;
|
||||
foreach (var index in MetaExtensions.AllRelevant.Where(other.DoApplyMeta))
|
||||
design.GetDesignDataRef().SetMeta(index, other.DesignData.GetMeta(index));
|
||||
|
||||
if (!design.DesignData.IsHuman)
|
||||
return;
|
||||
|
||||
ChangeEntireCustomize(design, other.DesignData.Customize, other.ApplyCustomize);
|
||||
|
||||
_forceFullItemOff = true;
|
||||
foreach (var slot in EquipSlotExtensions.FullSlots)
|
||||
{
|
||||
ChangeEquip(design, slot,
|
||||
other.DoApplyEquip(slot) ? other.DesignData.Item(slot) : null,
|
||||
other.DoApplyStain(slot) ? other.DesignData.Stain(slot) : null);
|
||||
}
|
||||
|
||||
_forceFullItemOff = false;
|
||||
|
||||
foreach (var slot in BonusExtensions.AllFlags)
|
||||
{
|
||||
if (other.DoApplyBonusItem(slot))
|
||||
ChangeBonusItem(design, slot, other.DesignData.BonusItem(slot));
|
||||
}
|
||||
|
||||
foreach (var slot in Enum.GetValues<CrestFlag>().Where(other.DoApplyCrest))
|
||||
ChangeCrest(design, slot, other.DesignData.Crest(slot));
|
||||
|
||||
foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter))
|
||||
ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]);
|
||||
|
||||
foreach (var (key, value) in other.Materials)
|
||||
{
|
||||
if (!value.Enabled)
|
||||
continue;
|
||||
|
||||
design.GetMaterialDataRef().AddOrUpdateValue(MaterialValueIndex.FromKey(key), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. </summary>
|
||||
private bool ChangeMainhandPeriphery(DesignBase design, EquipItem currentMain, EquipItem currentOff, EquipItem newMain,
|
||||
out EquipItem? newOff,
|
||||
out EquipItem? newGauntlets)
|
||||
{
|
||||
newOff = null;
|
||||
newGauntlets = null;
|
||||
if (newMain.Type != currentMain.Type)
|
||||
{
|
||||
var defaultOffhand = Items.GetDefaultOffhand(newMain);
|
||||
if (!Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o))
|
||||
return false;
|
||||
|
||||
newOff = o;
|
||||
}
|
||||
else if (!_forceFullItemOff && Config.ChangeEntireItem && newMain.Type is not FullEquipType.Sword) // Skip applying shields.
|
||||
{
|
||||
var defaultOffhand = Items.GetDefaultOffhand(newMain);
|
||||
if (Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o))
|
||||
newOff = o;
|
||||
|
||||
if (newMain.Type is FullEquipType.Fists && Items.ItemData.Tertiary.TryGetValue(newMain.ItemId, out var g))
|
||||
newGauntlets = g;
|
||||
}
|
||||
|
||||
if (!design.GetDesignDataRef().SetItem(EquipSlot.MainHand, newMain))
|
||||
return false;
|
||||
|
||||
if (newOff.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.OffHand, newOff.Value))
|
||||
{
|
||||
design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newGauntlets.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.Hands, newGauntlets.Value))
|
||||
{
|
||||
design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain);
|
||||
design.GetDesignDataRef().SetItem(EquipSlot.OffHand, currentOff);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Glamourer.Designs.History;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
|
@ -47,11 +41,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
|
|||
|
||||
public struct CreationDate : ISortMode<Design>
|
||||
{
|
||||
public string Name
|
||||
=> "Creation Date (Older First)";
|
||||
public ReadOnlySpan<byte> Name
|
||||
=> "Creation Date (Older First)"u8;
|
||||
|
||||
public string Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date.";
|
||||
public ReadOnlySpan<byte> Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."u8;
|
||||
|
||||
public IEnumerable<IPath> GetChildren(Folder f)
|
||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate));
|
||||
|
|
@ -59,11 +53,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
|
|||
|
||||
public struct UpdateDate : ISortMode<Design>
|
||||
{
|
||||
public string Name
|
||||
=> "Update Date (Older First)";
|
||||
public ReadOnlySpan<byte> Name
|
||||
=> "Update Date (Older First)"u8;
|
||||
|
||||
public string Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date.";
|
||||
public ReadOnlySpan<byte> Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."u8;
|
||||
|
||||
public IEnumerable<IPath> GetChildren(Folder f)
|
||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit));
|
||||
|
|
@ -71,11 +65,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
|
|||
|
||||
public struct InverseCreationDate : ISortMode<Design>
|
||||
{
|
||||
public string Name
|
||||
=> "Creation Date (Newer First)";
|
||||
public ReadOnlySpan<byte> Name
|
||||
=> "Creation Date (Newer First)"u8;
|
||||
|
||||
public string Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date.";
|
||||
public ReadOnlySpan<byte> Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."u8;
|
||||
|
||||
public IEnumerable<IPath> GetChildren(Folder f)
|
||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate));
|
||||
|
|
@ -83,11 +77,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
|
|||
|
||||
public struct InverseUpdateDate : ISortMode<Design>
|
||||
{
|
||||
public string Name
|
||||
=> "Update Date (Newer First)";
|
||||
public ReadOnlySpan<byte> Name
|
||||
=> "Update Date (Newer First)"u8;
|
||||
|
||||
public string Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date.";
|
||||
public ReadOnlySpan<byte> Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."u8;
|
||||
|
||||
public IEnumerable<IPath> GetChildren(Folder f)
|
||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit));
|
||||
|
|
@ -99,34 +93,35 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
|
|||
_saveService.QueueSave(this);
|
||||
}
|
||||
|
||||
private void OnDesignChange(DesignChanged.Type type, Design design, object? data)
|
||||
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? data)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case DesignChanged.Type.Created:
|
||||
var parent = Root;
|
||||
if (data is string path)
|
||||
if ((data as CreationTransaction?)?.Path is { } path)
|
||||
try
|
||||
{
|
||||
parent = FindOrCreateAllFolders(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.", NotificationType.Error);
|
||||
Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.",
|
||||
NotificationType.Error);
|
||||
}
|
||||
|
||||
CreateDuplicateLeaf(parent, design.Name.Text, design);
|
||||
|
||||
return;
|
||||
case DesignChanged.Type.Deleted:
|
||||
if (FindLeaf(design, out var leaf1))
|
||||
if (TryGetValue(design, out var leaf1))
|
||||
Delete(leaf1);
|
||||
return;
|
||||
case DesignChanged.Type.ReloadedAll:
|
||||
Reload();
|
||||
return;
|
||||
case DesignChanged.Type.Renamed when data is string oldName:
|
||||
if (!FindLeaf(design, out var leaf2))
|
||||
case DesignChanged.Type.Renamed when (data as RenameTransaction?)?.Old is { } oldName:
|
||||
if (!TryGetValue(design, out var leaf2))
|
||||
return;
|
||||
|
||||
var old = oldName.FixName();
|
||||
|
|
@ -155,15 +150,6 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
|
|||
? (string.Empty, false)
|
||||
: (DesignToIdentifier(design), true);
|
||||
|
||||
// Search the entire filesystem for the leaf corresponding to a design.
|
||||
public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf)
|
||||
{
|
||||
leaf = Root.GetAllDescendants(ISortMode<Design>.Lexicographical)
|
||||
.OfType<Leaf>()
|
||||
.FirstOrDefault(l => l.Value == design);
|
||||
return leaf != null;
|
||||
}
|
||||
|
||||
internal static void MigrateOldPaths(SaveService saveService, Dictionary<string, string> oldPaths)
|
||||
{
|
||||
if (oldPaths.Count == 0)
|
||||
|
|
|
|||
|
|
@ -1,76 +1,84 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Utility;
|
||||
using Glamourer.Customization;
|
||||
using Dalamud.Utility;
|
||||
using Glamourer.Designs.History;
|
||||
using Glamourer.Designs.Links;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.State;
|
||||
using OtterGui.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public class DesignManager
|
||||
public sealed class DesignManager : DesignEditor
|
||||
{
|
||||
private readonly CustomizationService _customizations;
|
||||
private readonly ItemManager _items;
|
||||
private readonly HumanModelList _humans;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly DesignChanged _event;
|
||||
private readonly List<Design> _designs = new();
|
||||
public readonly DesignStorage Designs;
|
||||
private readonly HumanModelList _humans;
|
||||
|
||||
public IReadOnlyList<Design> Designs
|
||||
=> _designs;
|
||||
|
||||
public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations,
|
||||
DesignChanged @event, HumanModelList humans)
|
||||
public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations,
|
||||
DesignChanged @event, HumanModelList humans, DesignStorage storage, DesignLinkLoader designLinkLoader, Configuration config)
|
||||
: base(saveService, @event, customizations, items, config)
|
||||
{
|
||||
_saveService = saveService;
|
||||
_items = items;
|
||||
_customizations = customizations;
|
||||
_event = @event;
|
||||
_humans = humans;
|
||||
Designs = storage;
|
||||
_humans = humans;
|
||||
|
||||
LoadDesigns(designLinkLoader);
|
||||
CreateDesignFolder(saveService);
|
||||
LoadDesigns();
|
||||
MigrateOldDesigns();
|
||||
designLinkLoader.SetAllObjects();
|
||||
}
|
||||
|
||||
#region Design Management
|
||||
|
||||
/// <summary>
|
||||
/// Clear currently loaded designs and load all designs anew from file.
|
||||
/// Invalid data is fixed, but changes are not saved until manual changes.
|
||||
/// </summary>
|
||||
public void LoadDesigns()
|
||||
private void LoadDesigns(DesignLinkLoader linkLoader)
|
||||
{
|
||||
_designs.Clear();
|
||||
List<(Design, string)> invalidNames = new();
|
||||
var skipped = 0;
|
||||
foreach (var file in _saveService.FileNames.Designs())
|
||||
_humans.Awaiter.Wait();
|
||||
Customizations.Awaiter.Wait();
|
||||
Items.ItemData.Awaiter.Wait();
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
Designs.Clear();
|
||||
var skipped = 0;
|
||||
ThreadLocal<List<(Design, string)>> designs = new(() => [], true);
|
||||
Parallel.ForEach(SaveService.FileNames.Designs(), (f, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(file.FullName);
|
||||
var text = File.ReadAllText(f.FullName);
|
||||
var data = JObject.Parse(text);
|
||||
var design = Design.LoadDesign(_customizations, _items, data);
|
||||
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(file.Name))
|
||||
invalidNames.Add((design, file.FullName));
|
||||
if (_designs.Any(f => f.Identifier == design.Identifier))
|
||||
throw new Exception($"Identifier {design.Identifier} was not unique.");
|
||||
|
||||
design.Index = _designs.Count;
|
||||
_designs.Add(design);
|
||||
var design = Design.LoadDesign(SaveService, Customizations, Items, linkLoader, data);
|
||||
designs.Value!.Add((design, f.FullName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Log.Error($"Could not load design, skipped:\n{ex}");
|
||||
++skipped;
|
||||
Interlocked.Increment(ref skipped);
|
||||
}
|
||||
});
|
||||
|
||||
List<(Design, string)> invalidNames = [];
|
||||
foreach (var (design, path) in designs.Values.SelectMany(v => v))
|
||||
{
|
||||
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(path))
|
||||
invalidNames.Add((design, path));
|
||||
if (Designs.Contains(design.Identifier))
|
||||
{
|
||||
Glamourer.Log.Error($"Could not load design, skipped: Identifier {design.Identifier} was not unique.");
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
|
||||
design.Index = Designs.Count;
|
||||
Designs.Add(design);
|
||||
}
|
||||
|
||||
var failed = MoveInvalidNames(invalidNames);
|
||||
|
|
@ -79,30 +87,35 @@ public class DesignManager
|
|||
$"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}");
|
||||
|
||||
Glamourer.Log.Information(
|
||||
$"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
|
||||
_event.Invoke(DesignChanged.Type.ReloadedAll, null!);
|
||||
$"Loaded {Designs.Count} designs in {stopwatch.ElapsedMilliseconds} ms.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ReloadedAll, null!, null);
|
||||
}
|
||||
|
||||
/// <summary> Create a new temporary design without adding it to the manager. </summary>
|
||||
public DesignBase CreateTemporary()
|
||||
=> new(_items);
|
||||
=> new(Customizations, Items);
|
||||
|
||||
/// <summary> Create a new design of a given name. </summary>
|
||||
public Design CreateEmpty(string name, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = ParseName(name, handlePath);
|
||||
var design = new Design(_customizations, _items)
|
||||
var design = new Design(Customizations, Items)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
LastEdit = DateTimeOffset.UtcNow,
|
||||
Identifier = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Index = _designs.Count,
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
LastEdit = DateTimeOffset.UtcNow,
|
||||
Identifier = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Index = Designs.Count,
|
||||
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
|
||||
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
|
||||
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
|
||||
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
|
||||
};
|
||||
_designs.Add(design);
|
||||
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
|
||||
Designs.Add(design);
|
||||
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
|
||||
_saveService.ImmediateSave(design);
|
||||
_event.Invoke(DesignChanged.Type.Created, design, path);
|
||||
SaveService.ImmediateSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
|
||||
return design;
|
||||
}
|
||||
|
||||
|
|
@ -112,17 +125,22 @@ public class DesignManager
|
|||
var (actualName, path) = ParseName(name, handlePath);
|
||||
var design = new Design(clone)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
LastEdit = DateTimeOffset.UtcNow,
|
||||
Identifier = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Index = _designs.Count,
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
LastEdit = DateTimeOffset.UtcNow,
|
||||
Identifier = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Index = Designs.Count,
|
||||
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
|
||||
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
|
||||
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
|
||||
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
|
||||
};
|
||||
|
||||
_designs.Add(design);
|
||||
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
|
||||
Designs.Add(design);
|
||||
Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design.");
|
||||
_saveService.ImmediateSave(design);
|
||||
_event.Invoke(DesignChanged.Type.Created, design, path);
|
||||
SaveService.ImmediateSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
|
||||
return design;
|
||||
}
|
||||
|
||||
|
|
@ -136,26 +154,31 @@ public class DesignManager
|
|||
LastEdit = DateTimeOffset.UtcNow,
|
||||
Identifier = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Index = _designs.Count,
|
||||
Index = Designs.Count,
|
||||
};
|
||||
_designs.Add(design);
|
||||
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
|
||||
Designs.Add(design);
|
||||
Glamourer.Log.Debug(
|
||||
$"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}.");
|
||||
_saveService.ImmediateSave(design);
|
||||
_event.Invoke(DesignChanged.Type.Created, design, path);
|
||||
SaveService.ImmediateSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
|
||||
return design;
|
||||
}
|
||||
|
||||
/// <summary> Delete a design. </summary>
|
||||
public void Delete(Design design)
|
||||
{
|
||||
foreach (var d in _designs.Skip(design.Index + 1))
|
||||
foreach (var d in Designs.Skip(design.Index + 1))
|
||||
--d.Index;
|
||||
_designs.RemoveAt(design.Index);
|
||||
_saveService.ImmediateDelete(design);
|
||||
_event.Invoke(DesignChanged.Type.Deleted, design);
|
||||
Designs.RemoveAt(design.Index);
|
||||
SaveService.ImmediateDelete(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Deleted, design, null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edit Information
|
||||
|
||||
/// <summary> Rename a design. </summary>
|
||||
public void Rename(Design design, string newName)
|
||||
{
|
||||
|
|
@ -165,9 +188,9 @@ public class DesignManager
|
|||
|
||||
design.Name = newName;
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.Renamed, design, oldName);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Renamed, design, new RenameTransaction(oldName, newName));
|
||||
}
|
||||
|
||||
/// <summary> Change the description of a design. </summary>
|
||||
|
|
@ -179,9 +202,23 @@ public class DesignManager
|
|||
|
||||
design.Description = description;
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Changed description of design {design.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription);
|
||||
DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, new DescriptionTransaction(oldDescription, description));
|
||||
}
|
||||
|
||||
/// <summary> Change the associated color of a design. </summary>
|
||||
public void ChangeColor(Design design, string newColor)
|
||||
{
|
||||
var oldColor = design.Color;
|
||||
if (oldColor == newColor)
|
||||
return;
|
||||
|
||||
design.Color = newColor;
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Changed color of design {design.Identifier}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, new DesignColorTransaction(oldColor, newColor));
|
||||
}
|
||||
|
||||
/// <summary> Add a new tag to a design. The tags remain sorted. </summary>
|
||||
|
|
@ -192,16 +229,12 @@ public class DesignManager
|
|||
|
||||
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
var idx = design.Tags.IndexOf(tag);
|
||||
_saveService.QueueSave(design);
|
||||
var idx = design.Tags.AsEnumerable().IndexOf(tag);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx));
|
||||
DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, new TagAddedTransaction(tag, idx));
|
||||
}
|
||||
|
||||
/// <summary> Remove a tag from a design if it exists. </summary>
|
||||
public void RemoveTag(Design design, string tag)
|
||||
=> RemoveTag(design, design.Tags.IndexOf(tag));
|
||||
|
||||
/// <summary> Remove a tag from a design by its index. </summary>
|
||||
public void RemoveTag(Design design, int tagIdx)
|
||||
{
|
||||
|
|
@ -211,9 +244,9 @@ public class DesignManager
|
|||
var oldTag = design.Tags[tagIdx];
|
||||
design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray();
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx));
|
||||
DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, new TagRemovedTransaction(oldTag, tagIdx));
|
||||
}
|
||||
|
||||
/// <summary> Rename a tag from a design by its index. The tags stay sorted.</summary>
|
||||
|
|
@ -226,9 +259,10 @@ public class DesignManager
|
|||
design.Tags[tagIdx] = newTag;
|
||||
Array.Sort(design.Tags);
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
|
||||
_event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx));
|
||||
DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design,
|
||||
new TagChangedTransaction(oldTag, newTag, tagIdx, design.Tags.AsEnumerable().IndexOf(newTag)));
|
||||
}
|
||||
|
||||
/// <summary> Add an associated mod to a design. </summary>
|
||||
|
|
@ -238,9 +272,9 @@ public class DesignManager
|
|||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings));
|
||||
DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings));
|
||||
}
|
||||
|
||||
/// <summary> Remove an associated mod from a design. </summary>
|
||||
|
|
@ -250,9 +284,28 @@ public class DesignManager
|
|||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings));
|
||||
DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, new ModRemovedTransaction(mod, settings));
|
||||
}
|
||||
|
||||
/// <summary> Add or update an associated mod to a design. </summary>
|
||||
public void UpdateMod(Design design, Mod mod, ModSettings settings)
|
||||
{
|
||||
var hasOldSettings = design.AssociatedMods.TryGetValue(mod, out var oldSettings);
|
||||
design.AssociatedMods[mod] = settings;
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
if (hasOldSettings)
|
||||
{
|
||||
Glamourer.Log.Debug($"Updated associated mod {mod.DirectoryName} from design {design.Identifier}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.UpdatedMod, design, new ModUpdatedTransaction(mod, oldSettings, settings));
|
||||
}
|
||||
else
|
||||
{
|
||||
Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} from design {design.Identifier}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Set the write protection status of a design. </summary>
|
||||
|
|
@ -261,260 +314,202 @@ public class DesignManager
|
|||
if (!design.SetWriteProtected(value))
|
||||
return;
|
||||
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected.");
|
||||
_event.Invoke(DesignChanged.Type.WriteProtection, design, value);
|
||||
DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, null);
|
||||
}
|
||||
|
||||
/// <summary> Change a customization value. </summary>
|
||||
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
|
||||
/// <summary> Set the quick design bar display status of a design. </summary>
|
||||
public void SetQuickDesign(Design design, bool value)
|
||||
{
|
||||
var oldValue = design.DesignData.Customize[idx];
|
||||
switch (idx)
|
||||
{
|
||||
case CustomizeIndex.Race:
|
||||
case CustomizeIndex.BodyType:
|
||||
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
|
||||
return;
|
||||
case CustomizeIndex.Clan:
|
||||
if (_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value) == 0)
|
||||
return;
|
||||
if (value == design.QuickDesign)
|
||||
return;
|
||||
|
||||
design.RemoveInvalidCustomize(_customizations);
|
||||
break;
|
||||
case CustomizeIndex.Gender:
|
||||
if (_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)) == 0)
|
||||
return;
|
||||
design.QuickDesign = value;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug(
|
||||
$"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, null);
|
||||
}
|
||||
|
||||
design.RemoveInvalidCustomize(_customizations);
|
||||
break;
|
||||
default:
|
||||
if (!_customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender,
|
||||
design.DesignData.Customize.Face, idx, value)
|
||||
|| !design.DesignData.Customize.Set(idx, value))
|
||||
return;
|
||||
#endregion
|
||||
|
||||
break;
|
||||
}
|
||||
#region Edit Application Rules
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
|
||||
_saveService.QueueSave(design);
|
||||
_event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx));
|
||||
public void ChangeForcedRedraw(Design design, bool forcedRedraw)
|
||||
{
|
||||
if (design.ForcedRedraw == forcedRedraw)
|
||||
return;
|
||||
|
||||
design.ForcedRedraw = forcedRedraw;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set {design.Identifier} to {(forcedRedraw ? string.Empty : "not")} force redraws.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ForceRedraw, design, null);
|
||||
}
|
||||
|
||||
public void ChangeResetAdvancedDyes(Design design, bool resetAdvancedDyes)
|
||||
{
|
||||
if (design.ResetAdvancedDyes == resetAdvancedDyes)
|
||||
return;
|
||||
|
||||
design.ResetAdvancedDyes = resetAdvancedDyes;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set {design.Identifier} to {(resetAdvancedDyes ? string.Empty : "not")} reset advanced dyes.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ResetAdvancedDyes, design, null);
|
||||
}
|
||||
|
||||
public void ChangeResetTemporarySettings(Design design, bool resetTemporarySettings)
|
||||
{
|
||||
if (design.ResetTemporarySettings == resetTemporarySettings)
|
||||
return;
|
||||
|
||||
design.ResetTemporarySettings = resetTemporarySettings;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set {design.Identifier} to {(resetTemporarySettings ? string.Empty : "not")} reset temporary settings.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ResetTemporarySettings, design, null);
|
||||
}
|
||||
|
||||
/// <summary> Change whether to apply a specific customize value. </summary>
|
||||
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
|
||||
{
|
||||
var set = _customizations.AwaitedService.GetList(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender);
|
||||
value &= set.IsAvailable(idx) || idx is CustomizeIndex.Clan or CustomizeIndex.Gender;
|
||||
if (!design.SetApplyCustomize(idx, value))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}.");
|
||||
_event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
|
||||
}
|
||||
|
||||
/// <summary> Change a non-weapon equipment piece. </summary>
|
||||
public void ChangeEquip(Design design, EquipSlot slot, EquipItem item)
|
||||
{
|
||||
if (!_items.IsItemValid(slot, item.ItemId, out item))
|
||||
return;
|
||||
|
||||
var old = design.DesignData.Item(slot);
|
||||
if (!design.DesignData.SetItem(slot, item))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
Glamourer.Log.Debug(
|
||||
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}).");
|
||||
_saveService.QueueSave(design);
|
||||
_event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot));
|
||||
}
|
||||
|
||||
/// <summary> Change a weapon. </summary>
|
||||
public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)
|
||||
{
|
||||
var currentMain = design.DesignData.Item(EquipSlot.MainHand);
|
||||
var currentOff = design.DesignData.Item(EquipSlot.OffHand);
|
||||
switch (slot)
|
||||
{
|
||||
case EquipSlot.MainHand:
|
||||
var newOff = currentOff;
|
||||
if (!_items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item))
|
||||
return;
|
||||
|
||||
if (item.Type != currentMain.Type)
|
||||
{
|
||||
var defaultOffhand = _items.GetDefaultOffhand(item);
|
||||
if (!_items.IsOffhandValid(item, defaultOffhand.ItemId, out newOff))
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(design.DesignData.SetItem(EquipSlot.MainHand, item) | design.DesignData.SetItem(EquipSlot.OffHand, newOff)))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
Glamourer.Log.Debug(
|
||||
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId}).");
|
||||
_event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff));
|
||||
|
||||
return;
|
||||
case EquipSlot.OffHand:
|
||||
if (!_items.IsOffhandValid(currentOff.Type, item.ItemId, out item))
|
||||
return;
|
||||
|
||||
if (!design.DesignData.SetItem(EquipSlot.OffHand, item))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
Glamourer.Log.Debug(
|
||||
$"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId}).");
|
||||
_event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item));
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, new ApplicationTransaction(idx, !value, value));
|
||||
}
|
||||
|
||||
/// <summary> Change whether to apply a specific equipment piece. </summary>
|
||||
public void ChangeApplyEquip(Design design, EquipSlot slot, bool value)
|
||||
public void ChangeApplyItem(Design design, EquipSlot slot, bool value)
|
||||
{
|
||||
if (!design.SetApplyEquip(slot, value))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
|
||||
_event.Invoke(DesignChanged.Type.ApplyEquip, design, slot);
|
||||
DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, new ApplicationTransaction((slot, false), !value, value));
|
||||
}
|
||||
|
||||
/// <summary> Change the stain for any equipment piece. </summary>
|
||||
public void ChangeStain(Design design, EquipSlot slot, StainId stain)
|
||||
/// <summary> Change whether to apply a specific equipment piece. </summary>
|
||||
public void ChangeApplyBonusItem(Design design, BonusItemFlag slot, bool value)
|
||||
{
|
||||
if (_items.ValidateStain(stain, out _, false).Length > 0)
|
||||
return;
|
||||
|
||||
var oldStain = design.DesignData.Stain(slot);
|
||||
if (!design.DesignData.SetStain(slot, stain))
|
||||
if (!design.SetApplyBonusItem(slot, value))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}.");
|
||||
_event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot));
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set applying of {slot} bonus item to {value}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ApplyBonusItem, design, new ApplicationTransaction(slot, !value, value));
|
||||
}
|
||||
|
||||
/// <summary> Change whether to apply a specific stain. </summary>
|
||||
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
|
||||
public void ChangeApplyStains(Design design, EquipSlot slot, bool value)
|
||||
{
|
||||
if (!design.SetApplyStain(slot, value))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
|
||||
_event.Invoke(DesignChanged.Type.ApplyStain, design, slot);
|
||||
DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, new ApplicationTransaction((slot, true), !value, value));
|
||||
}
|
||||
|
||||
/// <summary> Change the bool value of one of the meta flags. </summary>
|
||||
public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
|
||||
/// <summary> Change whether to apply a specific crest visibility. </summary>
|
||||
public void ChangeApplyCrest(Design design, CrestFlag slot, bool value)
|
||||
{
|
||||
var change = metaIndex switch
|
||||
{
|
||||
ActorState.MetaIndex.Wetness => design.DesignData.SetIsWet(value),
|
||||
ActorState.MetaIndex.HatState => design.DesignData.SetHatVisible(value),
|
||||
ActorState.MetaIndex.VisorState => design.DesignData.SetVisor(value),
|
||||
ActorState.MetaIndex.WeaponState => design.DesignData.SetWeaponVisible(value),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null),
|
||||
};
|
||||
if (!change)
|
||||
if (!design.SetApplyCrest(slot, value))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set value of {metaIndex} to {value}.");
|
||||
_event.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value));
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, new ApplicationTransaction(slot, !value, value));
|
||||
}
|
||||
|
||||
/// <summary> Change the application value of one of the meta flags. </summary>
|
||||
public void ChangeApplyMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
|
||||
public void ChangeApplyMeta(Design design, MetaIndex metaIndex, bool value)
|
||||
{
|
||||
var change = metaIndex switch
|
||||
{
|
||||
ActorState.MetaIndex.Wetness => design.SetApplyWetness(value),
|
||||
ActorState.MetaIndex.HatState => design.SetApplyHatVisible(value),
|
||||
ActorState.MetaIndex.VisorState => design.SetApplyVisorToggle(value),
|
||||
ActorState.MetaIndex.WeaponState => design.SetApplyWeaponVisible(value),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null),
|
||||
};
|
||||
if (!change)
|
||||
if (!design.SetApplyMeta(metaIndex, value))
|
||||
return;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(design);
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}.");
|
||||
_event.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value));
|
||||
DesignChanged.Invoke(DesignChanged.Type.Other, design, new ApplicationTransaction(metaIndex, !value, value));
|
||||
}
|
||||
|
||||
/// <summary> Apply an entire design based on its appliance rules piece by piece. </summary>
|
||||
public void ApplyDesign(Design design, DesignBase other)
|
||||
/// <summary> Change the application value of a customize parameter. </summary>
|
||||
public void ChangeApplyParameter(Design design, CustomizeParameterFlag flag, bool value)
|
||||
{
|
||||
if (other.DoApplyWetness())
|
||||
design.DesignData.SetIsWet(other.DesignData.IsWet());
|
||||
if (other.DoApplyHatVisible())
|
||||
design.DesignData.SetHatVisible(other.DesignData.IsHatVisible());
|
||||
if (other.DoApplyVisorToggle())
|
||||
design.DesignData.SetVisor(other.DesignData.IsVisorToggled());
|
||||
if (other.DoApplyWeaponVisible())
|
||||
design.DesignData.SetWeaponVisible(other.DesignData.IsWeaponVisible());
|
||||
if (!design.SetApplyParameter(flag, value))
|
||||
return;
|
||||
|
||||
if (design.DesignData.IsHuman)
|
||||
{
|
||||
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
if (other.DoApplyCustomize(index))
|
||||
ChangeCustomize(design, index, other.DesignData.Customize[index]);
|
||||
}
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
SaveService.QueueSave(design);
|
||||
Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}.");
|
||||
DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, new ApplicationTransaction(flag, !value, value));
|
||||
}
|
||||
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
if (other.DoApplyEquip(slot))
|
||||
ChangeEquip(design, slot, other.DesignData.Item(slot));
|
||||
/// <summary> Change multiple application values at once. </summary>
|
||||
public void ChangeApplyMulti(Design design, bool? equipment, bool? customization, bool? bonus, bool? parameters, bool? meta, bool? stains,
|
||||
bool? materials, bool? crest)
|
||||
{
|
||||
if (equipment is { } e)
|
||||
foreach (var f in EquipSlotExtensions.FullSlots)
|
||||
ChangeApplyItem(design, f, e);
|
||||
if (stains is { } s)
|
||||
foreach (var f in EquipSlotExtensions.FullSlots)
|
||||
ChangeApplyStains(design, f, s);
|
||||
if (customization is { } c)
|
||||
foreach (var f in CustomizationExtensions.All.Where(design.CustomizeSet.IsAvailable).Prepend(CustomizeIndex.Clan)
|
||||
.Prepend(CustomizeIndex.Gender))
|
||||
ChangeApplyCustomize(design, f, c);
|
||||
if (bonus is { } b)
|
||||
foreach (var f in BonusExtensions.AllFlags)
|
||||
ChangeApplyBonusItem(design, f, b);
|
||||
if (meta is { } m)
|
||||
foreach (var f in MetaExtensions.AllRelevant)
|
||||
ChangeApplyMeta(design, f, m);
|
||||
if (crest is { } cr)
|
||||
foreach (var f in CrestExtensions.AllRelevantSet)
|
||||
ChangeApplyCrest(design, f, cr);
|
||||
|
||||
if (other.DoApplyStain(slot))
|
||||
ChangeStain(design, slot, other.DesignData.Stain(slot));
|
||||
}
|
||||
}
|
||||
if (parameters is { } p)
|
||||
foreach (var f in CustomizeParameterExtensions.AllFlags)
|
||||
ChangeApplyParameter(design, f, p);
|
||||
|
||||
if (other.DoApplyEquip(EquipSlot.MainHand))
|
||||
ChangeWeapon(design, EquipSlot.MainHand, other.DesignData.Item(EquipSlot.MainHand));
|
||||
if (materials is { } ma)
|
||||
foreach (var (key, _) in design.GetMaterialData().ToArray())
|
||||
ChangeApplyMaterialValue(design, MaterialValueIndex.FromKey(key), ma);
|
||||
}
|
||||
|
||||
if (other.DoApplyEquip(EquipSlot.OffHand))
|
||||
ChangeWeapon(design, EquipSlot.OffHand, other.DesignData.Item(EquipSlot.OffHand));
|
||||
#endregion
|
||||
|
||||
if (other.DoApplyStain(EquipSlot.MainHand))
|
||||
ChangeStain(design, EquipSlot.MainHand, other.DesignData.Stain(EquipSlot.MainHand));
|
||||
public void UndoDesignChange(Design design)
|
||||
{
|
||||
if (!UndoStore.Remove(design.Identifier, out var otherData))
|
||||
return;
|
||||
|
||||
if (other.DoApplyStain(EquipSlot.OffHand))
|
||||
ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand));
|
||||
var other = CreateTemporary();
|
||||
other.SetDesignData(Customizations, otherData);
|
||||
ApplyDesign(design, other);
|
||||
}
|
||||
|
||||
private void MigrateOldDesigns()
|
||||
{
|
||||
if (!File.Exists(_saveService.FileNames.MigrationDesignFile))
|
||||
if (!File.Exists(SaveService.FileNames.MigrationDesignFile))
|
||||
return;
|
||||
|
||||
var errors = 0;
|
||||
var skips = 0;
|
||||
var successes = 0;
|
||||
var oldDesigns = _designs.ToList();
|
||||
var oldDesigns = Designs.ToList();
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(_saveService.FileNames.MigrationDesignFile);
|
||||
var text = File.ReadAllText(SaveService.FileNames.MigrationDesignFile);
|
||||
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(text) ?? new Dictionary<string, string>();
|
||||
var migratedFileSystemPaths = new Dictionary<string, string>(dict.Count);
|
||||
foreach (var (name, base64) in dict)
|
||||
|
|
@ -522,14 +517,14 @@ public class DesignManager
|
|||
try
|
||||
{
|
||||
var actualName = Path.GetFileName(name);
|
||||
var design = new Design(_customizations, _items)
|
||||
var design = new Design(Customizations, Items)
|
||||
{
|
||||
CreationDate = File.GetCreationTimeUtc(_saveService.FileNames.MigrationDesignFile),
|
||||
LastEdit = File.GetLastWriteTimeUtc(_saveService.FileNames.MigrationDesignFile),
|
||||
CreationDate = File.GetCreationTimeUtc(SaveService.FileNames.MigrationDesignFile),
|
||||
LastEdit = File.GetLastWriteTimeUtc(SaveService.FileNames.MigrationDesignFile),
|
||||
Identifier = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
};
|
||||
design.MigrateBase64(_items, _humans, base64);
|
||||
design.MigrateBase64(Customizations, Items, _humans, base64);
|
||||
if (!oldDesigns.Any(d => d.Name == design.Name && d.CreationDate == design.CreationDate))
|
||||
{
|
||||
Add(design, $"Migrated old design to {design.Identifier}.");
|
||||
|
|
@ -550,24 +545,24 @@ public class DesignManager
|
|||
}
|
||||
}
|
||||
|
||||
DesignFileSystem.MigrateOldPaths(_saveService, migratedFileSystemPaths);
|
||||
DesignFileSystem.MigrateOldPaths(SaveService, migratedFileSystemPaths);
|
||||
Glamourer.Log.Information(
|
||||
$"Successfully migrated {successes} old designs. Skipped {skips} already migrated designs. Failed to migrate {errors} designs.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Glamourer.Log.Error($"Could not migrate old design file {_saveService.FileNames.MigrationDesignFile}:\n{e}");
|
||||
Glamourer.Log.Error($"Could not migrate old design file {SaveService.FileNames.MigrationDesignFile}:\n{e}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(_saveService.FileNames.MigrationDesignFile,
|
||||
Path.ChangeExtension(_saveService.FileNames.MigrationDesignFile, ".json.bak"));
|
||||
Glamourer.Log.Information($"Moved migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file.");
|
||||
File.Move(SaveService.FileNames.MigrationDesignFile,
|
||||
Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak"), true);
|
||||
Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Log.Error($"Could not move migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file:\n{ex}");
|
||||
Glamourer.Log.Error($"Could not move migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -597,7 +592,7 @@ public class DesignManager
|
|||
{
|
||||
try
|
||||
{
|
||||
var correctName = _saveService.FileNames.DesignFile(design);
|
||||
var correctName = SaveService.FileNames.DesignFile(design);
|
||||
File.Move(name, correctName, false);
|
||||
Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
|
||||
}
|
||||
|
|
@ -617,7 +612,7 @@ public class DesignManager
|
|||
while (true)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
if (_designs.All(d => d.Identifier != guid))
|
||||
if (!Designs.Contains(guid))
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
|
|
@ -627,18 +622,17 @@ public class DesignManager
|
|||
/// Returns false if the design is already contained or if the identifier is already in use.
|
||||
/// The design is treated as newly created and invokes an event.
|
||||
/// </summary>
|
||||
private bool Add(Design design, string? message)
|
||||
private void Add(Design design, string? message)
|
||||
{
|
||||
if (_designs.Any(d => d == design || d.Identifier == design.Identifier))
|
||||
return false;
|
||||
if (Designs.Any(d => d == design || d.Identifier == design.Identifier))
|
||||
return;
|
||||
|
||||
design.Index = _designs.Count;
|
||||
_designs.Add(design);
|
||||
design.Index = Designs.Count;
|
||||
Designs.Add(design);
|
||||
if (!message.IsNullOrEmpty())
|
||||
Glamourer.Log.Debug(message);
|
||||
_saveService.ImmediateSave(design);
|
||||
_event.Invoke(DesignChanged.Type.Created, design);
|
||||
return true;
|
||||
SaveService.ImmediateSave(design);
|
||||
DesignChanged.Invoke(DesignChanged.Type.Created, design, null);
|
||||
}
|
||||
|
||||
/// <summary> Split a given string into its folder path and its name, if <paramref name="handlePath"/> is true. </summary>
|
||||
|
|
|
|||
18
Glamourer/Designs/DesignStorage.cs
Normal file
18
Glamourer/Designs/DesignStorage.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using OtterGui.Services;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public class DesignStorage : List<Design>, IService
|
||||
{
|
||||
public bool TryGetValue(Guid identifier, [NotNullWhen(true)] out Design? design)
|
||||
{
|
||||
design = ByIdentifier(identifier);
|
||||
return design != null;
|
||||
}
|
||||
|
||||
public Design? ByIdentifier(Guid identifier)
|
||||
=> this.FirstOrDefault(d => d.Identifier == identifier);
|
||||
|
||||
public bool Contains(Guid identifier)
|
||||
=> ByIdentifier(identifier) != null;
|
||||
}
|
||||
185
Glamourer/Designs/History/DesignTransaction.cs
Normal file
185
Glamourer/Designs/History/DesignTransaction.cs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
using Glamourer.GameData;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.State;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Designs.History;
|
||||
|
||||
/// <remarks> Only Designs. Can not be reverted. </remarks>
|
||||
public readonly record struct CreationTransaction(string Name, string? Path)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct RenameTransaction(string Old, string New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is RenameTransaction other ? new RenameTransaction(other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).Rename((Design)data, Old);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct DescriptionTransaction(string Old, string New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is DescriptionTransaction other ? new DescriptionTransaction(other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).ChangeDescription((Design)data, Old);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct DesignColorTransaction(string Old, string New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is DesignColorTransaction other ? new DesignColorTransaction(other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).ChangeColor((Design)data, Old);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct TagAddedTransaction(string New, int Index)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).RemoveTag((Design)data, Index);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct TagRemovedTransaction(string Old, int Index)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).AddTag((Design)data, Old);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct TagChangedTransaction(string Old, string New, int IndexOld, int IndexNew)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is TagChangedTransaction other && other.IndexNew == IndexOld
|
||||
? new TagChangedTransaction(other.Old, New, other.IndexOld, IndexNew)
|
||||
: null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).RenameTag((Design)data, IndexNew, Old);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct ModAddedTransaction(Mod Mod, ModSettings Settings)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).RemoveMod((Design)data, Mod);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct ModRemovedTransaction(Mod Mod, ModSettings Settings)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).AddMod((Design)data, Mod, Settings);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct ModUpdatedTransaction(Mod Mod, ModSettings Old, ModSettings New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is ModUpdatedTransaction other && Mod == other.Mod ? new ModUpdatedTransaction(Mod, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).UpdateMod((Design)data, Mod, Old);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct MaterialTransaction(MaterialValueIndex Index, ColorRow? Old, ColorRow? New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is MaterialTransaction other && Index == other.Index ? new MaterialTransaction(Index, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
{
|
||||
if (editor is DesignManager e)
|
||||
e.ChangeMaterialValue((Design)data, Index, Old);
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct MaterialRevertTransaction(MaterialValueIndex Index, bool Old, bool New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> ((DesignManager)editor).ChangeMaterialRevert((Design)data, Index, Old);
|
||||
}
|
||||
|
||||
/// <remarks> Only Designs. </remarks>
|
||||
public readonly record struct ApplicationTransaction(object Index, bool Old, bool New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
{
|
||||
var manager = (DesignManager)editor;
|
||||
var design = (Design)data;
|
||||
switch (Index)
|
||||
{
|
||||
case CustomizeIndex idx:
|
||||
manager.ChangeApplyCustomize(design, idx, Old);
|
||||
break;
|
||||
case (EquipSlot slot, true):
|
||||
manager.ChangeApplyStains(design, slot, Old);
|
||||
break;
|
||||
case (EquipSlot slot, _):
|
||||
manager.ChangeApplyItem(design, slot, Old);
|
||||
break;
|
||||
case BonusItemFlag slot:
|
||||
manager.ChangeApplyBonusItem(design, slot, Old);
|
||||
break;
|
||||
case CrestFlag slot:
|
||||
manager.ChangeApplyCrest(design, slot, Old);
|
||||
break;
|
||||
case MetaIndex slot:
|
||||
manager.ChangeApplyMeta(design, slot, Old);
|
||||
break;
|
||||
case CustomizeParameterFlag slot:
|
||||
manager.ChangeApplyParameter(design, slot, Old);
|
||||
break;
|
||||
case MaterialValueIndex slot:
|
||||
manager.ChangeApplyMaterialValue(design, slot, Old);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
191
Glamourer/Designs/History/EditorHistory.cs
Normal file
191
Glamourer/Designs/History/EditorHistory.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.State;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Designs.History;
|
||||
|
||||
public class EditorHistory : IDisposable, IService
|
||||
{
|
||||
public const int MaxUndo = 16;
|
||||
|
||||
private sealed class Queue : IReadOnlyList<ITransaction>
|
||||
{
|
||||
private DateTime _lastAdd = DateTime.UtcNow;
|
||||
|
||||
private readonly ITransaction[] _data = new ITransaction[MaxUndo];
|
||||
public int Offset { get; private set; }
|
||||
public int Count { get; private set; }
|
||||
|
||||
public void Add(ITransaction transaction)
|
||||
{
|
||||
if (!TryMerge(transaction))
|
||||
{
|
||||
if (Count == MaxUndo)
|
||||
{
|
||||
_data[Offset] = transaction;
|
||||
Offset = (Offset + 1) % MaxUndo;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Offset > 0)
|
||||
{
|
||||
_data[(Count + Offset) % MaxUndo] = transaction;
|
||||
++Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
_data[Count] = transaction;
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lastAdd = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private bool TryMerge(ITransaction newTransaction)
|
||||
{
|
||||
if (Count == 0)
|
||||
return false;
|
||||
|
||||
var time = DateTime.UtcNow;
|
||||
if (time - _lastAdd > TimeSpan.FromMilliseconds(250))
|
||||
return false;
|
||||
|
||||
var lastIdx = (Offset + Count - 1) % MaxUndo;
|
||||
if (newTransaction.Merge(_data[lastIdx]) is not { } transaction)
|
||||
return false;
|
||||
|
||||
_data[lastIdx] = transaction;
|
||||
return true;
|
||||
}
|
||||
|
||||
public ITransaction? RemoveLast()
|
||||
{
|
||||
if (Count == 0)
|
||||
return null;
|
||||
|
||||
--Count;
|
||||
var idx = (Offset + Count) % MaxUndo;
|
||||
return _data[idx];
|
||||
}
|
||||
|
||||
public IEnumerator<ITransaction> GetEnumerator()
|
||||
{
|
||||
var end = Offset + (Offset + Count) % MaxUndo;
|
||||
for (var i = Offset; i < end; ++i)
|
||||
yield return _data[i];
|
||||
|
||||
end = Count - end;
|
||||
for (var i = 0; i < end; ++i)
|
||||
yield return _data[i];
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public ITransaction this[int index]
|
||||
=> index < 0 || index >= Count
|
||||
? throw new IndexOutOfRangeException()
|
||||
: _data[(Offset + index) % MaxUndo];
|
||||
}
|
||||
|
||||
private readonly DesignEditor _designEditor;
|
||||
private readonly StateEditor _stateEditor;
|
||||
private readonly DesignChanged _designChanged;
|
||||
private readonly StateChanged _stateChanged;
|
||||
|
||||
private readonly Dictionary<ActorState, Queue> _stateEntries = [];
|
||||
private readonly Dictionary<Design, Queue> _designEntries = [];
|
||||
|
||||
private bool _undoMode;
|
||||
|
||||
public EditorHistory(DesignManager designEditor, StateManager stateEditor, DesignChanged designChanged, StateChanged stateChanged)
|
||||
{
|
||||
_designEditor = designEditor;
|
||||
_stateEditor = stateEditor;
|
||||
_designChanged = designChanged;
|
||||
_stateChanged = stateChanged;
|
||||
|
||||
_designChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.EditorHistory);
|
||||
_stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.EditorHistory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_designChanged.Unsubscribe(OnDesignChanged);
|
||||
_stateChanged.Unsubscribe(OnStateChanged);
|
||||
}
|
||||
|
||||
public bool CanUndo(ActorState state)
|
||||
=> _stateEntries.TryGetValue(state, out var list) && list.Count > 0;
|
||||
|
||||
public bool CanUndo(Design design)
|
||||
=> _designEntries.TryGetValue(design, out var list) && list.Count > 0;
|
||||
|
||||
public bool Undo(ActorState state)
|
||||
{
|
||||
if (!_stateEntries.TryGetValue(state, out var list) || list.Count == 0)
|
||||
return false;
|
||||
|
||||
_undoMode = true;
|
||||
list.RemoveLast()!.Revert(_stateEditor, state);
|
||||
_undoMode = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Undo(Design design)
|
||||
{
|
||||
if (!_designEntries.TryGetValue(design, out var list) || list.Count == 0)
|
||||
return false;
|
||||
|
||||
_undoMode = true;
|
||||
list.RemoveLast()!.Revert(_designEditor, design);
|
||||
_undoMode = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void AddStateTransaction(ActorState state, ITransaction transaction)
|
||||
{
|
||||
if (!_stateEntries.TryGetValue(state, out var list))
|
||||
{
|
||||
list = [];
|
||||
_stateEntries.Add(state, list);
|
||||
}
|
||||
|
||||
list.Add(transaction);
|
||||
}
|
||||
|
||||
private void AddDesignTransaction(Design design, ITransaction transaction)
|
||||
{
|
||||
if (!_designEntries.TryGetValue(design, out var list))
|
||||
{
|
||||
list = [];
|
||||
_designEntries.Add(design, list);
|
||||
}
|
||||
|
||||
list.Add(transaction);
|
||||
}
|
||||
|
||||
|
||||
private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData actors, ITransaction? data)
|
||||
{
|
||||
if (_undoMode || source is not StateSource.Manual)
|
||||
return;
|
||||
|
||||
if (data is not null)
|
||||
AddStateTransaction(state, data);
|
||||
}
|
||||
|
||||
private void OnDesignChanged(DesignChanged.Type type, Design design, ITransaction? data)
|
||||
{
|
||||
if (_undoMode)
|
||||
return;
|
||||
|
||||
if (data is not null)
|
||||
AddDesignTransaction(design, data);
|
||||
}
|
||||
}
|
||||
113
Glamourer/Designs/History/Transaction.cs
Normal file
113
Glamourer/Designs/History/Transaction.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Glamourer.GameData;
|
||||
|
||||
namespace Glamourer.Designs.History;
|
||||
|
||||
public interface ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction other);
|
||||
public void Revert(IDesignEditor editor, object data);
|
||||
}
|
||||
|
||||
public readonly record struct CustomizeTransaction(CustomizeIndex Slot, CustomizeValue Old, CustomizeValue New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is CustomizeTransaction other && Slot == other.Slot ? new CustomizeTransaction(Slot, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeCustomize(data, Slot, Old, ApplySettings.Manual);
|
||||
}
|
||||
|
||||
public readonly record struct EntireCustomizeTransaction(CustomizeFlag Apply, CustomizeArray Old, CustomizeArray New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is EntireCustomizeTransaction other ? new EntireCustomizeTransaction(Apply | other.Apply, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeEntireCustomize(data, Old, Apply, ApplySettings.Manual);
|
||||
}
|
||||
|
||||
public readonly record struct EquipTransaction(EquipSlot Slot, EquipItem Old, EquipItem New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is EquipTransaction other && Slot == other.Slot ? new EquipTransaction(Slot, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeItem(data, Slot, Old, ApplySettings.Manual);
|
||||
}
|
||||
|
||||
public readonly record struct BonusItemTransaction(BonusItemFlag Slot, EquipItem Old, EquipItem New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is BonusItemTransaction other && Slot == other.Slot ? new BonusItemTransaction(Slot, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeBonusItem(data, Slot, Old, ApplySettings.Manual);
|
||||
}
|
||||
|
||||
public readonly record struct WeaponTransaction(
|
||||
EquipItem OldMain,
|
||||
EquipItem OldOff,
|
||||
EquipItem OldGauntlets,
|
||||
EquipItem NewMain,
|
||||
EquipItem NewOff,
|
||||
EquipItem NewGauntlets)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is WeaponTransaction other
|
||||
? new WeaponTransaction(other.OldMain, other.OldOff, other.OldGauntlets, NewMain, NewOff, NewGauntlets)
|
||||
: null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
{
|
||||
editor.ChangeItem(data, EquipSlot.MainHand, OldMain, ApplySettings.Manual);
|
||||
editor.ChangeItem(data, EquipSlot.OffHand, OldOff, ApplySettings.Manual);
|
||||
editor.ChangeItem(data, EquipSlot.Hands, OldGauntlets, ApplySettings.Manual);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct StainTransaction(EquipSlot Slot, StainIds Old, StainIds New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is StainTransaction other && Slot == other.Slot ? new StainTransaction(Slot, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeStains(data, Slot, Old, ApplySettings.Manual);
|
||||
}
|
||||
|
||||
public readonly record struct CrestTransaction(CrestFlag Slot, bool Old, bool New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is CrestTransaction other && Slot == other.Slot ? new CrestTransaction(Slot, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeCrest(data, Slot, Old, ApplySettings.Manual);
|
||||
}
|
||||
|
||||
public readonly record struct ParameterTransaction(CustomizeParameterFlag Slot, CustomizeParameterValue Old, CustomizeParameterValue New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> older is ParameterTransaction other && Slot == other.Slot ? new ParameterTransaction(Slot, other.Old, New) : null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeCustomizeParameter(data, Slot, Old, ApplySettings.Manual);
|
||||
}
|
||||
|
||||
public readonly record struct MetaTransaction(MetaIndex Slot, bool Old, bool New)
|
||||
: ITransaction
|
||||
{
|
||||
public ITransaction? Merge(ITransaction older)
|
||||
=> null;
|
||||
|
||||
public void Revert(IDesignEditor editor, object data)
|
||||
=> editor.ChangeMetaState(data, Slot, Old, ApplySettings.Manual);
|
||||
}
|
||||
92
Glamourer/Designs/IDesignEditor.cs
Normal file
92
Glamourer/Designs/IDesignEditor.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using Glamourer.Designs.Links;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.State;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public readonly record struct ApplySettings(
|
||||
uint Key = 0,
|
||||
StateSource Source = StateSource.Manual,
|
||||
bool RespectManual = false,
|
||||
bool FromJobChange = false,
|
||||
bool UseSingleSource = false,
|
||||
bool MergeLinks = false,
|
||||
bool ResetMaterials = false,
|
||||
bool IsFinal = false)
|
||||
{
|
||||
public static readonly ApplySettings Manual = new()
|
||||
{
|
||||
Key = 0,
|
||||
Source = StateSource.Manual,
|
||||
FromJobChange = false,
|
||||
RespectManual = false,
|
||||
UseSingleSource = false,
|
||||
MergeLinks = false,
|
||||
ResetMaterials = false,
|
||||
IsFinal = false,
|
||||
};
|
||||
|
||||
public static readonly ApplySettings ManualWithLinks = new()
|
||||
{
|
||||
Key = 0,
|
||||
Source = StateSource.Manual,
|
||||
FromJobChange = false,
|
||||
RespectManual = false,
|
||||
UseSingleSource = false,
|
||||
MergeLinks = true,
|
||||
ResetMaterials = false,
|
||||
IsFinal = false,
|
||||
};
|
||||
|
||||
public static readonly ApplySettings Game = new()
|
||||
{
|
||||
Key = 0,
|
||||
Source = StateSource.Game,
|
||||
FromJobChange = false,
|
||||
RespectManual = false,
|
||||
UseSingleSource = false,
|
||||
MergeLinks = false,
|
||||
ResetMaterials = true,
|
||||
IsFinal = false,
|
||||
};
|
||||
}
|
||||
|
||||
public interface IDesignEditor
|
||||
{
|
||||
/// <summary> Change a customization value. </summary>
|
||||
public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change an entire customize array according to the given flags. </summary>
|
||||
public void ChangeEntireCustomize(object data, in CustomizeArray customizeInput, CustomizeFlag apply, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change a customize parameter. </summary>
|
||||
public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue v, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change an equipment piece. </summary>
|
||||
public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default)
|
||||
=> ChangeEquip(data, slot, item, null, settings);
|
||||
|
||||
/// <summary> Change a bonus item. </summary>
|
||||
public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change the stain for any equipment piece. </summary>
|
||||
public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings = default)
|
||||
=> ChangeEquip(data, slot, null, stains, settings);
|
||||
|
||||
/// <summary> Change an equipment piece and its stain at the same time. </summary>
|
||||
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change the crest visibility for any equipment piece. </summary>
|
||||
public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change the bool value of one of the meta flags. </summary>
|
||||
public void ChangeMetaState(object data, MetaIndex slot, bool value, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change all values applies from the given design. </summary>
|
||||
public void ApplyDesign(object data, MergedDesign design, ApplySettings settings = default);
|
||||
|
||||
/// <summary> Change all values applies from the given design. </summary>
|
||||
public void ApplyDesign(object data, DesignBase design, ApplySettings settings = default);
|
||||
}
|
||||
31
Glamourer/Designs/IDesignStandIn.cs
Normal file
31
Glamourer/Designs/IDesignStandIn.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Glamourer.Automation;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.State;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public interface IDesignStandIn : IEquatable<IDesignStandIn>
|
||||
{
|
||||
public string ResolveName(bool incognito);
|
||||
public ref readonly DesignData GetDesignData(in DesignData baseRef);
|
||||
|
||||
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData();
|
||||
|
||||
public string SerializeName();
|
||||
public StateSource AssociatedSource();
|
||||
|
||||
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication);
|
||||
|
||||
public void AddData(JObject jObj);
|
||||
|
||||
public void ParseData(JObject jObj);
|
||||
|
||||
public bool ChangeData(object data);
|
||||
|
||||
public bool ForcedRedraw { get; }
|
||||
|
||||
public bool ResetAdvancedDyes { get; }
|
||||
public bool ResetTemporarySettings { get; }
|
||||
}
|
||||
19
Glamourer/Designs/Links/DesignLink.cs
Normal file
19
Glamourer/Designs/Links/DesignLink.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Glamourer.Automation;
|
||||
|
||||
namespace Glamourer.Designs.Links;
|
||||
|
||||
public record struct DesignLink(Design Link, ApplicationType Type);
|
||||
|
||||
public readonly record struct LinkData(Guid Identity, ApplicationType Type, LinkOrder Order)
|
||||
{
|
||||
public override string ToString()
|
||||
=> Identity.ToString();
|
||||
}
|
||||
|
||||
public enum LinkOrder : byte
|
||||
{
|
||||
Self,
|
||||
After,
|
||||
Before,
|
||||
None,
|
||||
};
|
||||
28
Glamourer/Designs/Links/DesignLinkLoader.cs
Normal file
28
Glamourer/Designs/Links/DesignLinkLoader.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Notification = OtterGui.Classes.Notification;
|
||||
|
||||
namespace Glamourer.Designs.Links;
|
||||
|
||||
public sealed class DesignLinkLoader(DesignStorage designStorage, MessageService messager)
|
||||
: DelayedReferenceLoader<Design, LinkData>(messager), IService
|
||||
{
|
||||
protected override bool TryGetObject(LinkData data, [NotNullWhen(true)] out Design? obj)
|
||||
=> designStorage.FindFirst(d => d.Identifier == data.Identity, out obj);
|
||||
|
||||
protected override bool SetObject(Design parent, Design child, LinkData data, out string error)
|
||||
=> LinkContainer.AddLink(parent, child, data.Type, data.Order, out error);
|
||||
|
||||
protected override void HandleChildNotFound(Design parent, LinkData data)
|
||||
{
|
||||
Messager.AddMessage(new Notification(
|
||||
$"Could not find the design {data.Identity}. If this design was deleted, please re-save {parent.Identifier}.",
|
||||
NotificationType.Warning));
|
||||
}
|
||||
|
||||
protected override void HandleChildNotSet(Design parent, Design child, string error)
|
||||
=> Messager.AddMessage(new Notification($"Could not link {child.Identifier} to {parent.Identifier}: {error}",
|
||||
NotificationType.Warning));
|
||||
}
|
||||
86
Glamourer/Designs/Links/DesignLinkManager.cs
Normal file
86
Glamourer/Designs/Links/DesignLinkManager.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using Glamourer.Automation;
|
||||
using Glamourer.Designs.History;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Services;
|
||||
using OtterGui.Services;
|
||||
|
||||
namespace Glamourer.Designs.Links;
|
||||
|
||||
public sealed class DesignLinkManager : IService, IDisposable
|
||||
{
|
||||
private readonly DesignStorage _storage;
|
||||
private readonly DesignChanged _event;
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
public DesignLinkManager(DesignStorage storage, DesignChanged @event, SaveService saveService)
|
||||
{
|
||||
_storage = storage;
|
||||
_event = @event;
|
||||
_saveService = saveService;
|
||||
|
||||
_event.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignLinkManager);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _event.Unsubscribe(OnDesignChanged);
|
||||
|
||||
public void MoveDesignLink(Design parent, int idxFrom, LinkOrder orderFrom, int idxTo, LinkOrder orderTo)
|
||||
{
|
||||
if (!parent.Links.Reorder(idxFrom, orderFrom, idxTo, orderTo))
|
||||
return;
|
||||
|
||||
parent.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(parent);
|
||||
Glamourer.Log.Debug($"Moved link from {orderFrom} {idxFrom} to {idxTo} {orderTo}.");
|
||||
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
|
||||
}
|
||||
|
||||
public void AddDesignLink(Design parent, Design child, LinkOrder order)
|
||||
{
|
||||
if (!LinkContainer.AddLink(parent, child, ApplicationType.All, order, out _))
|
||||
return;
|
||||
|
||||
parent.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(parent);
|
||||
Glamourer.Log.Debug($"Added new {order} link to {child.Identifier} for {parent.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
|
||||
}
|
||||
|
||||
public void RemoveDesignLink(Design parent, int idx, LinkOrder order)
|
||||
{
|
||||
if (!parent.Links.Remove(idx, order))
|
||||
return;
|
||||
|
||||
parent.LastEdit = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(parent);
|
||||
Glamourer.Log.Debug($"Removed the {order} link at {idx} for {parent.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
|
||||
}
|
||||
|
||||
public void ChangeApplicationType(Design parent, int idx, LinkOrder order, ApplicationType applicationType)
|
||||
{
|
||||
applicationType &= ApplicationType.All;
|
||||
if (!parent.Links.ChangeApplicationRules(idx, order, applicationType, out var old))
|
||||
return;
|
||||
|
||||
_saveService.QueueSave(parent);
|
||||
Glamourer.Log.Debug($"Changed link application type from {old} to {applicationType} for design link {order} {idx + 1} in design {parent.Identifier}.");
|
||||
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
|
||||
}
|
||||
|
||||
private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, ITransaction? _)
|
||||
{
|
||||
if (type is not DesignChanged.Type.Deleted)
|
||||
return;
|
||||
|
||||
foreach (var design in _storage)
|
||||
{
|
||||
if (!design.Links.Remove(deletedDesign))
|
||||
continue;
|
||||
|
||||
design.LastEdit = DateTimeOffset.UtcNow;
|
||||
Glamourer.Log.Debug($"Removed {deletedDesign.Identifier} from {design.Identifier} links due to deletion.");
|
||||
_saveService.QueueSave(design);
|
||||
}
|
||||
}
|
||||
}
|
||||
328
Glamourer/Designs/Links/DesignMerger.cs
Normal file
328
Glamourer/Designs/Links/DesignMerger.cs
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Automation;
|
||||
using Glamourer.Designs.Special;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.State;
|
||||
using Glamourer.Unlocks;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs.Links;
|
||||
|
||||
public class DesignMerger(
|
||||
DesignManager designManager,
|
||||
CustomizeService _customize,
|
||||
Configuration _config,
|
||||
ItemUnlockManager _itemUnlocks,
|
||||
CustomizeUnlockManager _customizeUnlocks) : IService
|
||||
{
|
||||
public MergedDesign Merge(LinkContainer designs, in CustomizeArray currentCustomize, in DesignData baseRef, bool respectOwnership,
|
||||
bool modAssociations)
|
||||
=> Merge(designs.Select(d => ((IDesignStandIn)d.Link, d.Type, JobFlag.All)), currentCustomize, baseRef, respectOwnership,
|
||||
modAssociations);
|
||||
|
||||
public MergedDesign Merge(IEnumerable<(IDesignStandIn, ApplicationType, JobFlag)> designs, in CustomizeArray currentCustomize,
|
||||
in DesignData baseRef, bool respectOwnership, bool modAssociations)
|
||||
{
|
||||
var ret = new MergedDesign(designManager);
|
||||
ret.Design.SetCustomize(_customize, currentCustomize);
|
||||
var startBodyType = currentCustomize.BodyType;
|
||||
CustomizeFlag fixFlags = 0;
|
||||
respectOwnership &= _config.UnlockedItemMode;
|
||||
foreach (var (design, type, jobs) in designs)
|
||||
{
|
||||
if (type is 0)
|
||||
continue;
|
||||
|
||||
ref readonly var data = ref design.GetDesignData(baseRef);
|
||||
var source = design.AssociatedSource();
|
||||
|
||||
if (!data.IsHuman)
|
||||
continue;
|
||||
|
||||
var collection = type.ApplyWhat(design);
|
||||
ReduceMeta(data, collection.Meta, ret, source);
|
||||
ReduceCustomize(data, collection.Customize, ref fixFlags, ret, source, respectOwnership, startBodyType);
|
||||
ReduceEquip(data, collection.Equip, ret, source, respectOwnership);
|
||||
ReduceBonusItems(data, collection.BonusItem, ret, source, respectOwnership);
|
||||
ReduceMainhands(data, jobs, collection.Equip, ret, source, respectOwnership);
|
||||
ReduceOffhands(data, jobs, collection.Equip, ret, source, respectOwnership);
|
||||
ReduceCrests(data, collection.Crest, ret, source);
|
||||
ReduceParameters(data, collection.Parameters, ret, source);
|
||||
ReduceMods(design as Design, ret, modAssociations);
|
||||
if (type.HasFlag(ApplicationType.GearCustomization))
|
||||
ReduceMaterials(design, ret);
|
||||
if (design.ForcedRedraw)
|
||||
ret.ForcedRedraw = true;
|
||||
if (design.ResetAdvancedDyes)
|
||||
ret.ResetAdvancedDyes = true;
|
||||
if (design.ResetTemporarySettings)
|
||||
ret.ResetTemporarySettings = true;
|
||||
}
|
||||
|
||||
ApplyFixFlags(ret, fixFlags);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
private static void ReduceMaterials(IDesignStandIn designStandIn, MergedDesign ret)
|
||||
{
|
||||
if (designStandIn is not DesignBase design)
|
||||
return;
|
||||
|
||||
var materials = ret.Design.GetMaterialDataRef();
|
||||
foreach (var (key, value) in design.Materials.Where(p => p.Item2.Enabled))
|
||||
materials.TryAddValue(MaterialValueIndex.FromKey(key), value);
|
||||
}
|
||||
|
||||
private static void ReduceMods(Design? design, MergedDesign ret, bool modAssociations)
|
||||
{
|
||||
if (design == null || !modAssociations)
|
||||
return;
|
||||
|
||||
foreach (var (mod, settings) in design.AssociatedMods)
|
||||
ret.AssociatedMods.TryAdd(mod, settings);
|
||||
}
|
||||
|
||||
private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source)
|
||||
{
|
||||
applyMeta &= ~ret.Design.Application.Meta;
|
||||
if (applyMeta == 0)
|
||||
return;
|
||||
|
||||
foreach (var index in MetaExtensions.AllRelevant)
|
||||
{
|
||||
if (!applyMeta.HasFlag(index.ToFlag()))
|
||||
continue;
|
||||
|
||||
ret.Design.SetApplyMeta(index, true);
|
||||
ret.Design.GetDesignDataRef().SetMeta(index, design.GetMeta(index));
|
||||
ret.Sources[index] = source;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source)
|
||||
{
|
||||
crestFlags &= ~ret.Design.Application.Crest;
|
||||
if (crestFlags == 0)
|
||||
return;
|
||||
|
||||
foreach (var slot in CrestExtensions.AllRelevantSet)
|
||||
{
|
||||
if (!crestFlags.HasFlag(slot))
|
||||
continue;
|
||||
|
||||
ret.Design.GetDesignDataRef().SetCrest(slot, design.Crest(slot));
|
||||
ret.Design.SetApplyCrest(slot, true);
|
||||
ret.Sources[slot] = source;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret,
|
||||
StateSource source)
|
||||
{
|
||||
parameterFlags &= ~ret.Design.Application.Parameters;
|
||||
if (parameterFlags == 0)
|
||||
return;
|
||||
|
||||
foreach (var flag in CustomizeParameterExtensions.AllFlags)
|
||||
{
|
||||
if (!parameterFlags.HasFlag(flag))
|
||||
continue;
|
||||
|
||||
ret.Design.GetDesignDataRef().Parameters.Set(flag, design.Parameters[flag]);
|
||||
ret.Design.SetApplyParameter(flag, true);
|
||||
ret.Sources[flag] = source;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source,
|
||||
bool respectOwnership)
|
||||
{
|
||||
equipFlags &= ~ret.Design.Application.Equip;
|
||||
if (equipFlags == 0)
|
||||
return;
|
||||
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
var flag = slot.ToFlag();
|
||||
|
||||
if (equipFlags.HasFlag(flag))
|
||||
{
|
||||
var item = design.Item(slot);
|
||||
if (!respectOwnership || _itemUnlocks.IsUnlocked(item.Id, out _))
|
||||
ret.Design.GetDesignDataRef().SetItem(slot, item);
|
||||
ret.Design.SetApplyEquip(slot, true);
|
||||
ret.Sources[slot, false] = source;
|
||||
}
|
||||
|
||||
var stainFlag = slot.ToStainFlag();
|
||||
if (equipFlags.HasFlag(stainFlag))
|
||||
{
|
||||
ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot));
|
||||
ret.Design.SetApplyStain(slot, true);
|
||||
ret.Sources[slot, true] = source;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var slot in EquipSlotExtensions.WeaponSlots)
|
||||
{
|
||||
var stainFlag = slot.ToStainFlag();
|
||||
if (equipFlags.HasFlag(stainFlag))
|
||||
{
|
||||
ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot));
|
||||
ret.Design.SetApplyStain(slot, true);
|
||||
ret.Sources[slot, true] = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReduceBonusItems(in DesignData design, BonusItemFlag bonusItems, MergedDesign ret, StateSource source, bool respectOwnership)
|
||||
{
|
||||
bonusItems &= ~ret.Design.Application.BonusItem;
|
||||
if (bonusItems == 0)
|
||||
return;
|
||||
|
||||
foreach (var slot in BonusExtensions.AllFlags.Where(b => bonusItems.HasFlag(b)))
|
||||
{
|
||||
var item = design.BonusItem(slot);
|
||||
if (!respectOwnership || true) // TODO: maybe check unlocks
|
||||
ret.Design.GetDesignDataRef().SetBonusItem(slot, item);
|
||||
ret.Design.SetApplyBonusItem(slot, true);
|
||||
ret.Sources[slot] = source;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReduceMainhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source,
|
||||
bool respectOwnership)
|
||||
{
|
||||
if (!equipFlags.HasFlag(EquipFlag.Mainhand))
|
||||
return;
|
||||
|
||||
var weapon = design.Item(EquipSlot.MainHand);
|
||||
if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _))
|
||||
return;
|
||||
|
||||
if (!ret.Design.DoApplyEquip(EquipSlot.MainHand))
|
||||
{
|
||||
ret.Design.SetApplyEquip(EquipSlot.MainHand, true);
|
||||
ret.Design.GetDesignDataRef().SetItem(EquipSlot.MainHand, weapon);
|
||||
}
|
||||
|
||||
ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs);
|
||||
}
|
||||
|
||||
private void ReduceOffhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source,
|
||||
bool respectOwnership)
|
||||
{
|
||||
if (!equipFlags.HasFlag(EquipFlag.Offhand))
|
||||
return;
|
||||
|
||||
var weapon = design.Item(EquipSlot.OffHand);
|
||||
if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _))
|
||||
return;
|
||||
|
||||
if (!ret.Design.DoApplyEquip(EquipSlot.OffHand))
|
||||
{
|
||||
ret.Design.SetApplyEquip(EquipSlot.OffHand, true);
|
||||
ret.Design.GetDesignDataRef().SetItem(EquipSlot.OffHand, weapon);
|
||||
}
|
||||
|
||||
if (weapon.Valid)
|
||||
ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs);
|
||||
}
|
||||
|
||||
private void ReduceCustomize(in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag fixFlags, MergedDesign ret,
|
||||
StateSource source, bool respectOwnership, CustomizeValue startBodyType)
|
||||
{
|
||||
customizeFlags &= ~ret.Design.ApplyCustomizeExcludingBodyType;
|
||||
if (ret.Design.DesignData.Customize.BodyType != startBodyType)
|
||||
customizeFlags &= ~CustomizeFlag.BodyType;
|
||||
|
||||
if (customizeFlags == 0)
|
||||
return;
|
||||
|
||||
// Skip anything not human.
|
||||
if (!ret.Design.DesignData.IsHuman || !design.IsHuman)
|
||||
return;
|
||||
|
||||
var customize = ret.Design.DesignData.Customize;
|
||||
if (customizeFlags.HasFlag(CustomizeFlag.Clan))
|
||||
{
|
||||
fixFlags |= _customize.ChangeClan(ref customize, design.Customize.Clan);
|
||||
ret.Design.SetApplyCustomize(CustomizeIndex.Clan, true);
|
||||
ret.Design.SetApplyCustomize(CustomizeIndex.Race, true);
|
||||
customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race);
|
||||
ret.Sources[CustomizeIndex.Clan] = source;
|
||||
ret.Sources[CustomizeIndex.Race] = source;
|
||||
}
|
||||
|
||||
if (customizeFlags.HasFlag(CustomizeFlag.Gender))
|
||||
{
|
||||
fixFlags |= _customize.ChangeGender(ref customize, design.Customize.Gender);
|
||||
ret.Design.SetApplyCustomize(CustomizeIndex.Gender, true);
|
||||
customizeFlags &= ~CustomizeFlag.Gender;
|
||||
ret.Sources[CustomizeIndex.Gender] = source;
|
||||
}
|
||||
|
||||
if (customizeFlags.HasFlag(CustomizeFlag.Face))
|
||||
{
|
||||
customize[CustomizeIndex.Face] = design.Customize.Face;
|
||||
ret.Design.SetApplyCustomize(CustomizeIndex.Face, true);
|
||||
customizeFlags &= ~CustomizeFlag.Face;
|
||||
ret.Sources[CustomizeIndex.Face] = source;
|
||||
}
|
||||
|
||||
if (customizeFlags.HasFlag(CustomizeFlag.BodyType))
|
||||
{
|
||||
customize[CustomizeIndex.BodyType] = design.Customize.BodyType;
|
||||
customizeFlags &= ~CustomizeFlag.BodyType;
|
||||
ret.Sources[CustomizeIndex.BodyType] = source;
|
||||
}
|
||||
|
||||
var set = _customize.Manager.GetSet(customize.Clan, customize.Gender);
|
||||
var face = customize.Face;
|
||||
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
var flag = index.ToFlag();
|
||||
if (!customizeFlags.HasFlag(flag))
|
||||
continue;
|
||||
|
||||
var value = design.Customize[index];
|
||||
if (!CustomizeService.IsCustomizationValid(set, face, index, value, out var data))
|
||||
continue;
|
||||
|
||||
if (data.HasValue && respectOwnership && !_customizeUnlocks.IsUnlocked(data.Value, out _))
|
||||
continue;
|
||||
|
||||
customize[index] = data?.Value ?? value;
|
||||
ret.Design.SetApplyCustomize(index, true);
|
||||
ret.Sources[index] = source;
|
||||
fixFlags &= ~flag;
|
||||
}
|
||||
|
||||
ret.Design.SetCustomize(_customize, customize);
|
||||
}
|
||||
|
||||
private static void ApplyFixFlags(MergedDesign ret, CustomizeFlag fixFlags)
|
||||
{
|
||||
if (fixFlags == 0)
|
||||
return;
|
||||
|
||||
var source = ret.Design.DoApplyCustomize(CustomizeIndex.Clan)
|
||||
? ret.Sources[CustomizeIndex.Clan]
|
||||
: ret.Sources[CustomizeIndex.Gender];
|
||||
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
var flag = index.ToFlag();
|
||||
if (!fixFlags.HasFlag(flag))
|
||||
continue;
|
||||
|
||||
ret.Sources[index] = source;
|
||||
ret.Design.SetApplyCustomize(index, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
205
Glamourer/Designs/Links/LinkContainer.cs
Normal file
205
Glamourer/Designs/Links/LinkContainer.cs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
using Glamourer.Automation;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Filesystem;
|
||||
|
||||
namespace Glamourer.Designs.Links;
|
||||
|
||||
public sealed class LinkContainer : List<DesignLink>
|
||||
{
|
||||
public List<DesignLink> Before
|
||||
=> this;
|
||||
|
||||
public readonly List<DesignLink> After = [];
|
||||
|
||||
public new int Count
|
||||
=> base.Count + After.Count;
|
||||
|
||||
public LinkContainer Clone()
|
||||
{
|
||||
var ret = new LinkContainer();
|
||||
ret.EnsureCapacity(base.Count);
|
||||
ret.After.EnsureCapacity(After.Count);
|
||||
ret.AddRange(this);
|
||||
ret.After.AddRange(After);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public bool Reorder(int fromIndex, LinkOrder fromOrder, int toIndex, LinkOrder toOrder)
|
||||
{
|
||||
var fromList = fromOrder switch
|
||||
{
|
||||
LinkOrder.Before => Before,
|
||||
LinkOrder.After => After,
|
||||
_ => throw new ArgumentException("Invalid link order."),
|
||||
};
|
||||
|
||||
var toList = toOrder switch
|
||||
{
|
||||
LinkOrder.Before => Before,
|
||||
LinkOrder.After => After,
|
||||
_ => throw new ArgumentException("Invalid link order."),
|
||||
};
|
||||
|
||||
if (fromList == toList)
|
||||
return fromList.Move(fromIndex, toIndex);
|
||||
|
||||
if (fromIndex < 0 || fromIndex >= fromList.Count)
|
||||
return false;
|
||||
|
||||
toIndex = Math.Clamp(toIndex, 0, toList.Count);
|
||||
toList.Insert(toIndex, fromList[fromIndex]);
|
||||
fromList.RemoveAt(fromIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Remove(int idx, LinkOrder order)
|
||||
{
|
||||
var list = order switch
|
||||
{
|
||||
LinkOrder.Before => Before,
|
||||
LinkOrder.After => After,
|
||||
_ => throw new ArgumentException("Invalid link order."),
|
||||
};
|
||||
if (idx < 0 || idx >= list.Count)
|
||||
return false;
|
||||
|
||||
list.RemoveAt(idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ChangeApplicationRules(int idx, LinkOrder order, ApplicationType type, out ApplicationType old)
|
||||
{
|
||||
var list = order switch
|
||||
{
|
||||
LinkOrder.Before => Before,
|
||||
LinkOrder.After => After,
|
||||
_ => throw new ArgumentException("Invalid link order."),
|
||||
};
|
||||
old = list[idx].Type;
|
||||
if (idx < 0 || idx >= list.Count || old == type)
|
||||
return false;
|
||||
|
||||
list[idx] = list[idx] with { Type = type };
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanAddLink(Design parent, Design child, LinkOrder order, out string error)
|
||||
{
|
||||
if (parent == child)
|
||||
{
|
||||
error = $"Can not link {parent.Incognito} with itself.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent.Links.Contains(child))
|
||||
{
|
||||
error = $"Design {parent.Incognito} already contains a direct link to {child.Incognito}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetAllLinks(parent).Any(l => l.Link.Link == child && l.Order != order))
|
||||
{
|
||||
error =
|
||||
$"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the parent already links to the child in the opposite direction.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetAllLinks(child).Any(l => l.Link.Link == parent && l.Order == order))
|
||||
{
|
||||
error =
|
||||
$"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the child already links to the parent in the opposite direction.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool AddLink(Design parent, Design child, ApplicationType type, LinkOrder order, out string error)
|
||||
{
|
||||
if (!CanAddLink(parent, child, order, out error))
|
||||
return false;
|
||||
|
||||
var list = order switch
|
||||
{
|
||||
LinkOrder.Before => parent.Links.Before,
|
||||
LinkOrder.After => parent.Links.After,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
error = $"Order {order} is invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
type &= ApplicationType.All;
|
||||
list.Add(new DesignLink(child, type));
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Contains(Design child)
|
||||
=> Before.Any(l => l.Link == child) || After.Any(l => l.Link == child);
|
||||
|
||||
public bool Remove(Design child)
|
||||
=> Before.RemoveAll(l => l.Link == child) + After.RemoveAll(l => l.Link == child) > 0;
|
||||
|
||||
public static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(Design design)
|
||||
{
|
||||
var set = new HashSet<Design>(design.Links.Count * 4);
|
||||
return GetAllLinks(new DesignLink(design, ApplicationType.All), LinkOrder.Self, set);
|
||||
}
|
||||
|
||||
private static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(DesignLink design, LinkOrder currentOrder, ISet<Design> visited)
|
||||
{
|
||||
if (design.Link.Links.Count == 0)
|
||||
{
|
||||
if (visited.Add(design.Link))
|
||||
yield return (design, currentOrder);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var link in design.Link.Links.Before
|
||||
.Where(l => !visited.Contains(l.Link))
|
||||
.SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.After ? LinkOrder.After : LinkOrder.Before, visited)))
|
||||
yield return link;
|
||||
|
||||
if (visited.Add(design.Link))
|
||||
yield return (design, currentOrder);
|
||||
|
||||
foreach (var link in design.Link.Links.After.Where(l => !visited.Contains(l.Link))
|
||||
.SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.Before ? LinkOrder.Before : LinkOrder.After, visited)))
|
||||
yield return link;
|
||||
}
|
||||
|
||||
public JObject Serialize()
|
||||
{
|
||||
var before = new JArray();
|
||||
foreach (var link in Before)
|
||||
{
|
||||
before.Add(new JObject
|
||||
{
|
||||
["Design"] = link.Link.Identifier,
|
||||
["Type"] = (uint)link.Type,
|
||||
});
|
||||
}
|
||||
|
||||
var after = new JArray();
|
||||
foreach (var link in After)
|
||||
{
|
||||
after.Add(new JObject
|
||||
{
|
||||
["Design"] = link.Link.Identifier,
|
||||
["Type"] = (uint)link.Type,
|
||||
});
|
||||
}
|
||||
|
||||
return new JObject
|
||||
{
|
||||
[nameof(Before)] = before,
|
||||
[nameof(After)] = after,
|
||||
};
|
||||
}
|
||||
}
|
||||
105
Glamourer/Designs/Links/MergedDesign.cs
Normal file
105
Glamourer/Designs/Links/MergedDesign.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.State;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs.Links;
|
||||
|
||||
public readonly struct WeaponList
|
||||
{
|
||||
private readonly Dictionary<FullEquipType, List<(EquipItem, StateSource, JobFlag)>> _list = new(4);
|
||||
|
||||
public IEnumerable<(EquipItem, StateSource, JobFlag)> Values
|
||||
=> _list.Values.SelectMany(t => t);
|
||||
|
||||
public void Clear()
|
||||
=> _list.Clear();
|
||||
|
||||
public bool TryAdd(FullEquipType type, EquipItem item, StateSource source, JobFlag flags)
|
||||
{
|
||||
if (!_list.TryGetValue(type, out var list))
|
||||
{
|
||||
list = new List<(EquipItem, StateSource, JobFlag)>(2);
|
||||
_list.Add(type, list);
|
||||
}
|
||||
|
||||
var existingFlags = list.Count == 0 ? 0 : list.Select(t => t.Item3).Aggregate((t, existing) => t | existing);
|
||||
var remainingFlags = flags & ~existingFlags;
|
||||
|
||||
if (remainingFlags == 0)
|
||||
return false;
|
||||
|
||||
list.Add((item, source, remainingFlags));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGet(FullEquipType type, JobId id, bool gameStateAllowed, out (EquipItem, StateSource) ret)
|
||||
{
|
||||
if (!_list.TryGetValue(type, out var list))
|
||||
{
|
||||
ret = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var flag = (JobFlag)(1ul << id.Id);
|
||||
|
||||
foreach (var (item, source, flags) in list)
|
||||
{
|
||||
if (flags.HasFlag(flag) && (gameStateAllowed || source is not StateSource.Game))
|
||||
{
|
||||
ret = (item, source);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ret = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public WeaponList()
|
||||
{ }
|
||||
}
|
||||
|
||||
public sealed class MergedDesign
|
||||
{
|
||||
public MergedDesign(DesignManager designManager)
|
||||
{
|
||||
Design = designManager.CreateTemporary();
|
||||
Design.Application = ApplicationCollection.None;
|
||||
}
|
||||
|
||||
public MergedDesign(DesignBase design)
|
||||
{
|
||||
Design = design;
|
||||
if (design.DoApplyEquip(EquipSlot.MainHand))
|
||||
{
|
||||
var weapon = design.DesignData.Item(EquipSlot.MainHand);
|
||||
if (weapon.Valid)
|
||||
Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All);
|
||||
}
|
||||
|
||||
if (design.DoApplyEquip(EquipSlot.OffHand))
|
||||
{
|
||||
var weapon = design.DesignData.Item(EquipSlot.OffHand);
|
||||
if (weapon.Valid)
|
||||
Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All);
|
||||
}
|
||||
|
||||
ForcedRedraw = design is IDesignStandIn { ForcedRedraw: true };
|
||||
}
|
||||
|
||||
public MergedDesign(Design design)
|
||||
: this((DesignBase)design)
|
||||
{
|
||||
foreach (var (mod, settings) in design.AssociatedMods)
|
||||
AssociatedMods[mod] = settings;
|
||||
}
|
||||
|
||||
public readonly DesignBase Design;
|
||||
public readonly WeaponList Weapons = new();
|
||||
public readonly SortedList<Mod, ModSettings> AssociatedMods = [];
|
||||
public StateSources Sources = new();
|
||||
public bool ForcedRedraw;
|
||||
public bool ResetAdvancedDyes;
|
||||
public bool ResetTemporarySettings;
|
||||
}
|
||||
80
Glamourer/Designs/MetaIndex.cs
Normal file
80
Glamourer/Designs/MetaIndex.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using Glamourer.Api.Enums;
|
||||
using Glamourer.State;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public enum MetaIndex
|
||||
{
|
||||
Wetness = StateIndex.MetaWetness,
|
||||
HatState = StateIndex.MetaHatState,
|
||||
VisorState = StateIndex.MetaVisorState,
|
||||
WeaponState = StateIndex.MetaWeaponState,
|
||||
ModelId = StateIndex.MetaModelId,
|
||||
EarState = StateIndex.MetaEarState,
|
||||
}
|
||||
|
||||
public static class MetaExtensions
|
||||
{
|
||||
public static readonly IReadOnlyList<MetaIndex> AllRelevant =
|
||||
[MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState, MetaIndex.EarState];
|
||||
|
||||
public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState;
|
||||
|
||||
public static MetaFlag ToFlag(this MetaIndex index)
|
||||
=> index switch
|
||||
{
|
||||
MetaIndex.Wetness => MetaFlag.Wetness,
|
||||
MetaIndex.HatState => MetaFlag.HatState,
|
||||
MetaIndex.VisorState => MetaFlag.VisorState,
|
||||
MetaIndex.WeaponState => MetaFlag.WeaponState,
|
||||
MetaIndex.EarState => MetaFlag.EarState,
|
||||
_ => (MetaFlag)byte.MaxValue,
|
||||
};
|
||||
|
||||
public static MetaIndex ToIndex(this MetaFlag index)
|
||||
=> index switch
|
||||
{
|
||||
MetaFlag.Wetness => MetaIndex.Wetness,
|
||||
MetaFlag.HatState => MetaIndex.HatState,
|
||||
MetaFlag.VisorState => MetaIndex.VisorState,
|
||||
MetaFlag.WeaponState => MetaIndex.WeaponState,
|
||||
MetaFlag.EarState => MetaIndex.EarState,
|
||||
_ => (MetaIndex)byte.MaxValue,
|
||||
};
|
||||
|
||||
public static IEnumerable<MetaIndex> ToIndices(this MetaFlag index)
|
||||
{
|
||||
if (index.HasFlag(MetaFlag.Wetness))
|
||||
yield return MetaIndex.Wetness;
|
||||
if (index.HasFlag(MetaFlag.HatState))
|
||||
yield return MetaIndex.HatState;
|
||||
if (index.HasFlag(MetaFlag.VisorState))
|
||||
yield return MetaIndex.VisorState;
|
||||
if (index.HasFlag(MetaFlag.WeaponState))
|
||||
yield return MetaIndex.WeaponState;
|
||||
if (index.HasFlag(MetaFlag.EarState))
|
||||
yield return MetaIndex.EarState;
|
||||
}
|
||||
|
||||
public static string ToName(this MetaIndex index)
|
||||
=> index switch
|
||||
{
|
||||
MetaIndex.HatState => "Hat Visible",
|
||||
MetaIndex.VisorState => "Visor Toggled",
|
||||
MetaIndex.WeaponState => "Weapon Visible",
|
||||
MetaIndex.Wetness => "Force Wetness",
|
||||
MetaIndex.EarState => "Ears Visible",
|
||||
_ => "Unknown Meta",
|
||||
};
|
||||
|
||||
public static string ToTooltip(this MetaIndex index)
|
||||
=> index switch
|
||||
{
|
||||
MetaIndex.HatState => "Hide or show the characters head gear.",
|
||||
MetaIndex.VisorState => "Toggle the visor state of the characters head gear.",
|
||||
MetaIndex.WeaponState => "Hide or show the characters weapons when not drawn.",
|
||||
MetaIndex.Wetness => "Force the character to be wet or not.",
|
||||
MetaIndex.EarState => "Hide or show the characters ears through the head gear. (Viera only)",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
62
Glamourer/Designs/Special/QuickSelectedDesign.cs
Normal file
62
Glamourer/Designs/Special/QuickSelectedDesign.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Glamourer.Automation;
|
||||
using Glamourer.Gui;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.State;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs.Special;
|
||||
|
||||
public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IService
|
||||
{
|
||||
public const string SerializedName = "//QuickSelection";
|
||||
public const string ResolvedName = "Quick Design Bar Selection";
|
||||
|
||||
public bool Equals(IDesignStandIn? other)
|
||||
=> other is QuickSelectedDesign;
|
||||
|
||||
public string ResolveName(bool incognito)
|
||||
=> ResolvedName;
|
||||
|
||||
public Design? CurrentDesign
|
||||
=> combo.Design as Design;
|
||||
|
||||
public ref readonly DesignData GetDesignData(in DesignData baseRef)
|
||||
{
|
||||
if (combo.Design != null)
|
||||
return ref combo.Design.GetDesignData(baseRef);
|
||||
|
||||
return ref baseRef;
|
||||
}
|
||||
|
||||
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
|
||||
=> combo.Design?.GetMaterialData() ?? [];
|
||||
|
||||
public string SerializeName()
|
||||
=> SerializedName;
|
||||
|
||||
public StateSource AssociatedSource()
|
||||
=> StateSource.Manual;
|
||||
|
||||
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
|
||||
=> combo.Design?.AllLinks(newApplication) ?? [];
|
||||
|
||||
public void AddData(JObject jObj)
|
||||
{ }
|
||||
|
||||
public void ParseData(JObject jObj)
|
||||
{ }
|
||||
|
||||
public bool ChangeData(object data)
|
||||
=> false;
|
||||
|
||||
public bool ForcedRedraw
|
||||
=> combo.Design?.ForcedRedraw ?? false;
|
||||
|
||||
public bool ResetAdvancedDyes
|
||||
=> combo.Design?.ResetAdvancedDyes ?? false;
|
||||
|
||||
public bool ResetTemporarySettings
|
||||
=> combo.Design?.ResetTemporarySettings ?? false;
|
||||
}
|
||||
102
Glamourer/Designs/Special/RandomDesign.cs
Normal file
102
Glamourer/Designs/Special/RandomDesign.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using Glamourer.Automation;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.State;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs.Special;
|
||||
|
||||
public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn
|
||||
{
|
||||
public const string SerializedName = "//Random";
|
||||
public const string ResolvedName = "Random";
|
||||
private Design? _currentDesign;
|
||||
|
||||
public IReadOnlyList<IDesignPredicate> Predicates { get; private set; } = [];
|
||||
public bool ResetOnRedraw { get; set; } = false;
|
||||
|
||||
public string ResolveName(bool _)
|
||||
=> ResolvedName;
|
||||
|
||||
public ref readonly DesignData GetDesignData(in DesignData baseRef)
|
||||
{
|
||||
_currentDesign ??= rng.Design(Predicates);
|
||||
if (_currentDesign == null)
|
||||
return ref baseRef;
|
||||
|
||||
return ref _currentDesign.GetDesignDataRef();
|
||||
}
|
||||
|
||||
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
|
||||
{
|
||||
_currentDesign ??= rng.Design(Predicates);
|
||||
if (_currentDesign == null)
|
||||
return [];
|
||||
|
||||
return _currentDesign.Materials;
|
||||
}
|
||||
|
||||
public string SerializeName()
|
||||
=> SerializedName;
|
||||
|
||||
public bool Equals(IDesignStandIn? other)
|
||||
=> other is RandomDesign r
|
||||
&& r.ResetOnRedraw == ResetOnRedraw
|
||||
&& string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public StateSource AssociatedSource()
|
||||
=> StateSource.Manual;
|
||||
|
||||
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
|
||||
{
|
||||
if (newApplication || ResetOnRedraw)
|
||||
_currentDesign = rng.Design(Predicates);
|
||||
else
|
||||
_currentDesign ??= rng.Design(Predicates);
|
||||
if (_currentDesign == null)
|
||||
yield break;
|
||||
|
||||
foreach (var (link, type, jobs) in _currentDesign.AllLinks(newApplication))
|
||||
yield return (link, type, jobs);
|
||||
}
|
||||
|
||||
public void AddData(JObject jObj)
|
||||
{
|
||||
jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates);
|
||||
jObj["ResetOnRedraw"] = ResetOnRedraw;
|
||||
}
|
||||
|
||||
public void ParseData(JObject jObj)
|
||||
{
|
||||
var restrictions = jObj["Restrictions"]?.ToObject<string>() ?? string.Empty;
|
||||
Predicates = RandomPredicate.GeneratePredicates(restrictions);
|
||||
ResetOnRedraw = jObj["ResetOnRedraw"]?.ToObject<bool>() ?? false;
|
||||
}
|
||||
|
||||
public bool ChangeData(object data)
|
||||
{
|
||||
if (data is List<IDesignPredicate> predicates)
|
||||
{
|
||||
Predicates = predicates;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data is bool resetOnRedraw)
|
||||
{
|
||||
ResetOnRedraw = resetOnRedraw;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ForcedRedraw
|
||||
=> _currentDesign?.ForcedRedraw ?? false;
|
||||
|
||||
public bool ResetAdvancedDyes
|
||||
=> _currentDesign?.ResetAdvancedDyes ?? false;
|
||||
|
||||
public bool ResetTemporarySettings
|
||||
=> _currentDesign?.ResetTemporarySettings ?? false;
|
||||
}
|
||||
51
Glamourer/Designs/Special/RandomDesignGenerator.cs
Normal file
51
Glamourer/Designs/Special/RandomDesignGenerator.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Services;
|
||||
|
||||
namespace Glamourer.Designs.Special;
|
||||
|
||||
public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem, Configuration config) : IService
|
||||
{
|
||||
private readonly Random _rng = new();
|
||||
private readonly WeakReference<Design> _lastDesign = new(null!, false);
|
||||
|
||||
public Design? Design(IReadOnlyList<Design> localDesigns)
|
||||
{
|
||||
if (localDesigns.Count is 0)
|
||||
return null;
|
||||
|
||||
var idx = _rng.Next(0, localDesigns.Count);
|
||||
if (localDesigns.Count is 1)
|
||||
{
|
||||
_lastDesign.SetTarget(localDesigns[idx]);
|
||||
return localDesigns[idx];
|
||||
}
|
||||
|
||||
if (config.PreventRandomRepeats && _lastDesign.TryGetTarget(out var lastDesign))
|
||||
while (lastDesign == localDesigns[idx])
|
||||
idx = _rng.Next(0, localDesigns.Count);
|
||||
|
||||
var design = localDesigns[idx];
|
||||
Glamourer.Log.Verbose($"[Random Design] Chose design {idx + 1} out of {localDesigns.Count}: {design.Incognito}.");
|
||||
_lastDesign.SetTarget(design);
|
||||
return design;
|
||||
}
|
||||
|
||||
public Design? Design()
|
||||
=> Design(designs);
|
||||
|
||||
public Design? Design(IDesignPredicate predicate)
|
||||
=> Design(predicate.Get(designs, fileSystem).ToList());
|
||||
|
||||
public Design? Design(IReadOnlyList<IDesignPredicate> predicates)
|
||||
{
|
||||
return predicates.Count switch
|
||||
{
|
||||
0 => Design(),
|
||||
1 => Design(predicates[0]),
|
||||
_ => Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()),
|
||||
};
|
||||
}
|
||||
|
||||
public Design? Design(string restrictions)
|
||||
=> Design(RandomPredicate.GeneratePredicates(restrictions));
|
||||
}
|
||||
163
Glamourer/Designs/Special/RandomPredicate.cs
Normal file
163
Glamourer/Designs/Special/RandomPredicate.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Designs.Special;
|
||||
|
||||
public interface IDesignPredicate
|
||||
{
|
||||
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath);
|
||||
|
||||
public bool Invoke((Design Design, string LowerName, string Identifier, string LowerPath) args)
|
||||
=> Invoke(args.Design, args.LowerName, args.Identifier, args.LowerPath);
|
||||
|
||||
public IEnumerable<Design> Get(IEnumerable<Design> designs, DesignFileSystem fileSystem)
|
||||
=> designs.Select(d => Transform(d, fileSystem))
|
||||
.Where(Invoke)
|
||||
.Select(t => t.Design);
|
||||
|
||||
public static IEnumerable<Design> Get(IReadOnlyList<IDesignPredicate> predicates, IEnumerable<Design> designs, DesignFileSystem fileSystem)
|
||||
=> predicates.Count > 0
|
||||
? designs.Select(d => Transform(d, fileSystem))
|
||||
.Where(t => predicates.Any(p => p.Invoke(t)))
|
||||
.Select(t => t.Design)
|
||||
: designs;
|
||||
|
||||
private static (Design Design, string LowerName, string Identifier, string LowerPath) Transform(Design d, DesignFileSystem fs)
|
||||
=> (d, d.Name.Lower, d.Identifier.ToString(), fs.TryGetValue(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty);
|
||||
}
|
||||
|
||||
public static class RandomPredicate
|
||||
{
|
||||
public readonly struct StartsWith(string value) : IDesignPredicate
|
||||
{
|
||||
public LowerString Value { get; } = value;
|
||||
|
||||
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath)
|
||||
=> lowerPath.StartsWith(Value.Lower);
|
||||
|
||||
public override string ToString()
|
||||
=> $"/{Value.Text}";
|
||||
}
|
||||
|
||||
public readonly struct Contains(string value) : IDesignPredicate
|
||||
{
|
||||
public LowerString Value { get; } = value;
|
||||
|
||||
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath)
|
||||
{
|
||||
if (lowerName.Contains(Value.Lower))
|
||||
return true;
|
||||
if (identifier.Contains(Value.Lower))
|
||||
return true;
|
||||
if (lowerPath.Contains(Value.Lower))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> Value.Text;
|
||||
}
|
||||
|
||||
public readonly struct Exact(Exact.Type type, string value) : IDesignPredicate
|
||||
{
|
||||
public enum Type : byte
|
||||
{
|
||||
Name,
|
||||
Path,
|
||||
Identifier,
|
||||
Tag,
|
||||
Color,
|
||||
}
|
||||
|
||||
public Type Which { get; } = type;
|
||||
public LowerString Value { get; } = value;
|
||||
|
||||
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath)
|
||||
=> Which switch
|
||||
{
|
||||
Type.Name => lowerName == Value.Lower,
|
||||
Type.Path => lowerPath == Value.Lower,
|
||||
Type.Identifier => identifier == Value.Lower,
|
||||
Type.Tag => IsContained(Value, design.Tags),
|
||||
Type.Color => design.Color == Value,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static bool IsContained(LowerString value, IEnumerable<string> data)
|
||||
=> data.Any(t => t == value);
|
||||
|
||||
public override string ToString()
|
||||
=> $"\"{Which switch { Type.Name => 'n', Type.Identifier => 'i', Type.Path => 'p', Type.Tag => 't', Type.Color => 'c', _ => '?' }}?{Value.Text}\"";
|
||||
}
|
||||
|
||||
public static IDesignPredicate CreateSinglePredicate(string restriction)
|
||||
{
|
||||
switch (restriction[0])
|
||||
{
|
||||
case '/': return new StartsWith(restriction[1..]);
|
||||
case '"':
|
||||
var end = restriction.IndexOf('"', 1);
|
||||
if (end < 3)
|
||||
return new Contains(restriction);
|
||||
|
||||
switch (restriction[1], restriction[2])
|
||||
{
|
||||
case ('n', '?'):
|
||||
case ('N', '?'):
|
||||
return new Exact(Exact.Type.Name, restriction[3..end]);
|
||||
case ('p', '?'):
|
||||
case ('P', '?'):
|
||||
return new Exact(Exact.Type.Path, restriction[3..end]);
|
||||
case ('i', '?'):
|
||||
case ('I', '?'):
|
||||
return new Exact(Exact.Type.Identifier, restriction[3..end]);
|
||||
case ('t', '?'):
|
||||
case ('T', '?'):
|
||||
return new Exact(Exact.Type.Tag, restriction[3..end]);
|
||||
case ('c', '?'):
|
||||
case ('C', '?'):
|
||||
return new Exact(Exact.Type.Color, restriction[3..end]);
|
||||
default: return new Contains(restriction);
|
||||
}
|
||||
default: return new Contains(restriction);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<IDesignPredicate> GeneratePredicates(string restrictions)
|
||||
{
|
||||
if (restrictions.Length == 0)
|
||||
return [];
|
||||
|
||||
List<IDesignPredicate> predicates = new(1);
|
||||
if (restrictions[0] is '{')
|
||||
{
|
||||
var end = restrictions.IndexOf('}');
|
||||
if (end == -1)
|
||||
{
|
||||
predicates.Add(CreateSinglePredicate(restrictions));
|
||||
}
|
||||
else
|
||||
{
|
||||
restrictions = restrictions[1..end];
|
||||
var split = restrictions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
predicates.AddRange(split.Distinct().Select(CreateSinglePredicate));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
predicates.Add(CreateSinglePredicate(restrictions));
|
||||
}
|
||||
|
||||
return predicates;
|
||||
}
|
||||
|
||||
public static string GeneratePredicateString(IReadOnlyCollection<IDesignPredicate> predicates)
|
||||
{
|
||||
if (predicates.Count == 0)
|
||||
return string.Empty;
|
||||
if (predicates.Count == 1)
|
||||
return predicates.First()!.ToString()!;
|
||||
|
||||
return $"{{{string.Join("; ", predicates)}}}";
|
||||
}
|
||||
}
|
||||
54
Glamourer/Designs/Special/RevertDesign.cs
Normal file
54
Glamourer/Designs/Special/RevertDesign.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Glamourer.Automation;
|
||||
using Glamourer.Interop.Material;
|
||||
using Glamourer.State;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs.Special;
|
||||
|
||||
public class RevertDesign : IDesignStandIn
|
||||
{
|
||||
public const string SerializedName = "//Revert";
|
||||
public const string ResolvedName = "Revert";
|
||||
|
||||
public string ResolveName(bool _)
|
||||
=> ResolvedName;
|
||||
|
||||
public ref readonly DesignData GetDesignData(in DesignData baseRef)
|
||||
=> ref baseRef;
|
||||
|
||||
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
|
||||
=> [];
|
||||
|
||||
public string SerializeName()
|
||||
=> SerializedName;
|
||||
|
||||
public bool Equals(IDesignStandIn? other)
|
||||
=> other is RevertDesign;
|
||||
|
||||
public StateSource AssociatedSource()
|
||||
=> StateSource.Game;
|
||||
|
||||
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool _)
|
||||
{
|
||||
yield return (this, ApplicationType.All, JobFlag.All);
|
||||
}
|
||||
|
||||
public void AddData(JObject jObj)
|
||||
{ }
|
||||
|
||||
public void ParseData(JObject jObj)
|
||||
{ }
|
||||
|
||||
public bool ChangeData(object data)
|
||||
=> false;
|
||||
|
||||
public bool ForcedRedraw
|
||||
=> false;
|
||||
|
||||
public bool ResetAdvancedDyes
|
||||
=> true;
|
||||
|
||||
public bool ResetTemporarySettings
|
||||
=> true;
|
||||
}
|
||||
78
Glamourer/EphemeralConfig.cs
Normal file
78
Glamourer/EphemeralConfig.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Glamourer.Gui;
|
||||
using Glamourer.Services;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Classes;
|
||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||
|
||||
namespace Glamourer;
|
||||
|
||||
public class EphemeralConfig : ISavable
|
||||
{
|
||||
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
||||
public bool IncognitoMode { get; set; } = false;
|
||||
public bool UnlockDetailMode { get; set; } = true;
|
||||
public bool ShowDesignQuickBar { get; set; } = false;
|
||||
public bool LockDesignQuickBar { get; set; } = false;
|
||||
public bool LockMainWindow { get; set; } = false;
|
||||
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
|
||||
public Guid SelectedDesign { get; set; } = Guid.Empty;
|
||||
public Guid SelectedQuickDesign { get; set; } = Guid.Empty;
|
||||
public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion;
|
||||
|
||||
public float CurrentDesignSelectorWidth { get; set; } = 200f;
|
||||
public float DesignSelectorMinimumScale { get; set; } = 0.1f;
|
||||
public float DesignSelectorMaximumScale { get; set; } = 0.5f;
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
public EphemeralConfig(SaveService saveService)
|
||||
{
|
||||
_saveService = saveService;
|
||||
Load();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
=> _saveService.DelaySave(this, TimeSpan.FromSeconds(5));
|
||||
|
||||
public void Load()
|
||||
{
|
||||
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
|
||||
{
|
||||
Glamourer.Log.Error(
|
||||
$"Error parsing ephemeral Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
|
||||
errorArgs.ErrorContext.Handled = true;
|
||||
}
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.EphemeralConfigFile))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(_saveService.FileNames.EphemeralConfigFile);
|
||||
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
|
||||
{
|
||||
Error = HandleDeserializationError,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Messager.NotificationMessage(ex,
|
||||
"Error reading ephemeral Configuration, reverting to default.",
|
||||
"Error reading ephemeral Configuration", NotificationType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.EphemeralConfigFile;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var jWriter = new JsonTextWriter(writer);
|
||||
jWriter.Formatting = Formatting.Indented;
|
||||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(jWriter, this);
|
||||
}
|
||||
}
|
||||
16
Glamourer/Events/AutoRedrawChanged.cs
Normal file
16
Glamourer/Events/AutoRedrawChanged.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the auto-reload gear setting is changed in glamourer configuration.
|
||||
/// </summary>
|
||||
public sealed class AutoRedrawChanged()
|
||||
: EventWrapper<bool, AutoRedrawChanged.Priority>(nameof(AutoRedrawChanged))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.StateApi.OnGPoseChange"/>
|
||||
StateApi = int.MinValue,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Glamourer.Automation;
|
||||
using Glamourer.Automation;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
|
@ -12,8 +11,8 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is additional data depending on the type of change. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Type, AutoDesignSet?, object?>,
|
||||
AutomationChanged.Priority>
|
||||
public sealed class AutomationChanged()
|
||||
: EventWrapper<AutomationChanged.Type, AutoDesignSet?, object?, AutomationChanged.Priority>(nameof(AutomationChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
|
|
@ -38,6 +37,9 @@ public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Ty
|
|||
/// <summary> Change the used base state of a given set. Additional data is prior and new base. [(AutoDesignSet.Base, AutoDesignSet.Base)]. </summary>
|
||||
ChangedBase,
|
||||
|
||||
/// <summary> Change the resetting of temporary settings for a given set. Additional data is the new value. </summary>
|
||||
ChangedTemporarySettingsReset,
|
||||
|
||||
/// <summary> Add a new associated design to a given set. Additional data is the index it got added at [int]. </summary>
|
||||
AddedDesign,
|
||||
|
||||
|
|
@ -47,7 +49,7 @@ public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Ty
|
|||
/// <summary> Move a given associated design in the list of a given set. Additional data is the index that got moved and the index it got moved to [(int, int)]. </summary>
|
||||
MovedDesign,
|
||||
|
||||
/// <summary> Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, Design, Design)]. </summary>
|
||||
/// <summary> Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, IDesignStandIn, IDesignStandIn)]. </summary>
|
||||
ChangedDesign,
|
||||
|
||||
/// <summary> Change the job condition in an associated design for a given set. Additional data is the index of the changed associated design, the old job group and the new job group [(int, JobGroup, JobGroup)]. </summary>
|
||||
|
|
@ -55,6 +57,9 @@ public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Ty
|
|||
|
||||
/// <summary> Change the application type in an associated design for a given set. Additional data is the index of the changed associated design, the old type and the new type. [(int, AutoDesign.Type, AutoDesign.Type)]. </summary>
|
||||
ChangedType,
|
||||
|
||||
/// <summary> Change the additional data for a specific design type. Additional data is the index of the changed associated design and the new data. [(int, object)] </summary>
|
||||
ChangedData,
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
|
|
@ -63,13 +68,9 @@ public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Ty
|
|||
SetSelector = 0,
|
||||
|
||||
/// <seealso cref="AutoDesignApplier.OnAutomationChange"/>
|
||||
AutoDesignApplier,
|
||||
AutoDesignApplier = 0,
|
||||
|
||||
/// <seealso cref="Gui.Tabs.AutomationTab.RandomRestrictionDrawer.OnAutomationChange"/>
|
||||
RandomRestrictionDrawer = -1,
|
||||
}
|
||||
|
||||
public AutomationChanged()
|
||||
: base(nameof(AutomationChanged))
|
||||
{ }
|
||||
|
||||
public void Invoke(Type type, AutoDesignSet? set, object? data)
|
||||
=> Invoke(this, type, set, data);
|
||||
}
|
||||
|
|
|
|||
25
Glamourer/Events/BonusSlotUpdating.cs
Normal file
25
Glamourer/Events/BonusSlotUpdating.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when a model flags a bonus slot for an update.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the model with a flagged slot. </item>
|
||||
/// <item>Parameter is the bonus slot changed. </item>
|
||||
/// <item>Parameter is the model values to change the bonus piece to. </item>
|
||||
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class BonusSlotUpdating()
|
||||
: EventWrapperRef34<Model, BonusItemFlag, CharacterArmor, ulong, BonusSlotUpdating.Priority>(nameof(BonusSlotUpdating))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnBonusSlotUpdating"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Designs.History;
|
||||
using Glamourer.Gui;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
|
@ -12,84 +13,138 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is any additional data depending on the type of change. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Design, object?>, DesignChanged.Priority>
|
||||
public sealed class DesignChanged()
|
||||
: EventWrapper<DesignChanged.Type, Design, ITransaction?, DesignChanged.Priority>(nameof(DesignChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
/// <summary> A new design was created. Data is a potential path to move it to [string?]. </summary>
|
||||
/// <summary> A new design was created. </summary>
|
||||
Created,
|
||||
|
||||
/// <summary> An existing design was deleted. Data is null. </summary>
|
||||
/// <summary> An existing design was deleted. </summary>
|
||||
Deleted,
|
||||
|
||||
/// <summary> Invoked on full reload. Design and Data are null. </summary>
|
||||
/// <summary> Invoked on full reload. </summary>
|
||||
ReloadedAll,
|
||||
|
||||
/// <summary> An existing design was renamed. Data is the prior name [string]. </summary>
|
||||
/// <summary> An existing design was renamed. </summary>
|
||||
Renamed,
|
||||
|
||||
/// <summary> An existing design had its description changed. Data is the prior description [string]. </summary>
|
||||
/// <summary> An existing design had its description changed. </summary>
|
||||
ChangedDescription,
|
||||
|
||||
/// <summary> An existing design had a new tag added. Data is the new tag and the index it was added at [(string, int)]. </summary>
|
||||
/// <summary> An existing design had its associated color changed. </summary>
|
||||
ChangedColor,
|
||||
|
||||
/// <summary> An existing design had a new tag added. </summary>
|
||||
AddedTag,
|
||||
|
||||
/// <summary> An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. </summary>
|
||||
/// <summary> An existing design had an existing tag removed. </summary>
|
||||
RemovedTag,
|
||||
|
||||
/// <summary> An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. </summary>
|
||||
/// <summary> An existing design had an existing tag renamed. </summary>
|
||||
ChangedTag,
|
||||
|
||||
/// <summary> An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
|
||||
/// <summary> An existing design had a new associated mod added. </summary>
|
||||
AddedMod,
|
||||
|
||||
/// <summary> An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
|
||||
/// <summary> An existing design had an existing associated mod removed. </summary>
|
||||
RemovedMod,
|
||||
|
||||
/// <summary> An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. </summary>
|
||||
/// <summary> An existing design had an existing associated mod updated. </summary>
|
||||
UpdatedMod,
|
||||
|
||||
/// <summary> An existing design had a link to a different design added, removed or moved. </summary>
|
||||
ChangedLink,
|
||||
|
||||
/// <summary> An existing design had a customization changed. </summary>
|
||||
Customize,
|
||||
|
||||
/// <summary> An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. </summary>
|
||||
/// <summary> An existing design had its entire customize array changed. </summary>
|
||||
EntireCustomize,
|
||||
|
||||
/// <summary> An existing design had an equipment piece changed. </summary>
|
||||
Equip,
|
||||
|
||||
/// <summary> An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. </summary>
|
||||
/// <summary> An existing design had a bonus item changed. </summary>
|
||||
BonusItem,
|
||||
|
||||
/// <summary> An existing design had its weapons changed. </summary>
|
||||
Weapon,
|
||||
|
||||
/// <summary> An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
|
||||
Stain,
|
||||
/// <summary> An existing design had a stain changed. </summary>
|
||||
Stains,
|
||||
|
||||
/// <summary> An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. </summary>
|
||||
/// <summary> An existing design had a crest visibility changed. </summary>
|
||||
Crest,
|
||||
|
||||
/// <summary> An existing design had a customize parameter changed. </summary>
|
||||
Parameter,
|
||||
|
||||
/// <summary> An existing design had an advanced dye row added, changed, or deleted. </summary>
|
||||
Material,
|
||||
|
||||
/// <summary> An existing design had an advanced dye rows Revert state changed. </summary>
|
||||
MaterialRevert,
|
||||
|
||||
/// <summary> An existing design had changed whether it always forces a redraw or not. </summary>
|
||||
ForceRedraw,
|
||||
|
||||
/// <summary> An existing design had changed whether it always resets advanced dyes or not. </summary>
|
||||
ResetAdvancedDyes,
|
||||
|
||||
/// <summary> An existing design had changed whether it always resets all prior temporary settings or not. </summary>
|
||||
ResetTemporarySettings,
|
||||
|
||||
/// <summary> An existing design changed whether a specific customization is applied. </summary>
|
||||
ApplyCustomize,
|
||||
|
||||
/// <summary> An existing design changed whether a specific equipment is applied. Data is the slot of the equipment [EquipSlot]. </summary>
|
||||
/// <summary> An existing design changed whether a specific equipment piece is applied. </summary>
|
||||
ApplyEquip,
|
||||
|
||||
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
|
||||
/// <summary> An existing design changed whether a specific bonus item is applied. </summary>
|
||||
ApplyBonusItem,
|
||||
|
||||
/// <summary> An existing design changed whether a specific stain is applied. </summary>
|
||||
ApplyStain,
|
||||
|
||||
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary>
|
||||
/// <summary> An existing design changed whether a specific crest visibility is applied. </summary>
|
||||
ApplyCrest,
|
||||
|
||||
/// <summary> An existing design changed whether a specific customize parameter is applied. </summary>
|
||||
ApplyParameter,
|
||||
|
||||
/// <summary> An existing design changed whether an advanced dye row is applied. </summary>
|
||||
ApplyMaterial,
|
||||
|
||||
/// <summary> An existing design changed its write protection status. </summary>
|
||||
WriteProtection,
|
||||
|
||||
/// <summary> An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. </summary>
|
||||
/// <summary> An existing design changed its display status for the quick design bar. </summary>
|
||||
QuickDesignBar,
|
||||
|
||||
/// <summary> An existing design changed one of the meta flags. </summary>
|
||||
Other,
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChanged"/>
|
||||
DesignLinkManager = 1,
|
||||
|
||||
/// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/>
|
||||
AutoDesignManager = 1,
|
||||
|
||||
/// <seealso cref="DesignFileSystem.OnDesignChange"/>
|
||||
DesignFileSystem = 0,
|
||||
|
||||
/// <seealso cref="Gui.Tabs.DesignTab.DesignFileSystemSelector.OnDesignChange"/>
|
||||
DesignFileSystemSelector = -1,
|
||||
|
||||
/// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/>
|
||||
AutoDesignManager = 1,
|
||||
/// <seealso cref="DesignComboBase.OnDesignChanged"/>
|
||||
DesignCombo = -2,
|
||||
|
||||
/// <seealso cref="EditorHistory.OnDesignChanged" />
|
||||
EditorHistory = -1000,
|
||||
}
|
||||
|
||||
public DesignChanged()
|
||||
: base(nameof(DesignChanged))
|
||||
{ }
|
||||
|
||||
public void Invoke(Type type, Design design, object? data = null)
|
||||
=> Invoke(this, type, design, data);
|
||||
}
|
||||
|
|
|
|||
25
Glamourer/Events/EquipSlotUpdating.cs
Normal file
25
Glamourer/Events/EquipSlotUpdating.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when a model flags an equipment slot for an update.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the model with a flagged slot. </item>
|
||||
/// <item>Parameter is the equipment slot changed. </item>
|
||||
/// <item>Parameter is the model values to change the equipment piece to. </item>
|
||||
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class EquipSlotUpdating()
|
||||
: EventWrapperRef34<Model, EquipSlot, CharacterArmor, ulong, EquipSlotUpdating.Priority>(nameof(EquipSlotUpdating))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnEquipSlotUpdating"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
}
|
||||
23
Glamourer/Events/EquippedGearset.cs
Normal file
23
Glamourer/Events/EquippedGearset.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the player equips a gear set.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the name of the gear set. </item>
|
||||
/// <item>Parameter is the id of the gear set. </item>
|
||||
/// <item>Parameter is the id of the prior gear set. </item>
|
||||
/// <item>Parameter is the id of the associated glamour. </item>
|
||||
/// <item>Parameter is the job id of the associated job. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class EquippedGearset()
|
||||
: EventWrapper<string, int, int, byte, byte, EquippedGearset.Priority>(nameof(EquippedGearset))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Automation.AutoDesignApplier.OnEquippedGearset"/>
|
||||
AutoDesignApplier = 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
public sealed class GPoseService : EventWrapper<Action<bool>, GPoseService.Priority>
|
||||
public sealed class GPoseService : EventWrapper<bool, GPoseService.Priority>
|
||||
{
|
||||
private readonly IFramework _framework;
|
||||
private readonly IClientState _state;
|
||||
|
|
@ -15,8 +13,8 @@ public sealed class GPoseService : EventWrapper<Action<bool>, GPoseService.Prior
|
|||
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.GlamourerIpc.OnGPoseChanged"/>
|
||||
GlamourerIpc = int.MinValue,
|
||||
/// <seealso cref="Api.StateApi.OnGPoseChange"/>
|
||||
StateApi = int.MinValue,
|
||||
}
|
||||
|
||||
public bool InGPose { get; private set; }
|
||||
|
|
@ -56,9 +54,9 @@ public sealed class GPoseService : EventWrapper<Action<bool>, GPoseService.Prior
|
|||
return;
|
||||
|
||||
InGPose = inGPose;
|
||||
Invoke(this, InGPose);
|
||||
Invoke(InGPose);
|
||||
var actions = InGPose ? _onEnter : _onLeave;
|
||||
foreach (var action in actions)
|
||||
while (actions.TryDequeue(out var action))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
|||
21
Glamourer/Events/GearsetDataLoaded.cs
Normal file
21
Glamourer/Events/GearsetDataLoaded.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers when the equipped gearset finished all LoadEquipment, LoadWeapon, and LoadCrest calls. (All Non-MetaData)
|
||||
/// This defines an endpoint for when the gameState is updated.
|
||||
/// <list type="number">
|
||||
/// <item>The model draw object associated with the finished load (Also fired by other players on render) </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class GearsetDataLoaded()
|
||||
: EventWrapper<Actor, Model, GearsetDataLoaded.Priority>(nameof(GearsetDataLoaded))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnGearsetDataLoaded"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using Glamourer.Interop.Structs;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
|
|
@ -11,22 +10,12 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is the new state. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class HeadGearVisibilityChanged : EventWrapper<Action<Actor, Ref<bool>>, HeadGearVisibilityChanged.Priority>
|
||||
public sealed class HeadGearVisibilityChanged()
|
||||
: EventWrapperRef2<Actor, bool, HeadGearVisibilityChanged.Priority>(nameof(HeadGearVisibilityChanged))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnHeadGearVisibilityChange"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
|
||||
public HeadGearVisibilityChanged()
|
||||
: base(nameof(HeadGearVisibilityChanged))
|
||||
{ }
|
||||
|
||||
public void Invoke(Actor actor, ref bool state)
|
||||
{
|
||||
var value = new Ref<bool>(state);
|
||||
Invoke(this, actor, value);
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
|
@ -11,18 +10,12 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is an array of slots updated and corresponding item ids and stains. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class MovedEquipment : EventWrapper<Action<(EquipSlot, uint, StainId)[]>, MovedEquipment.Priority>
|
||||
public sealed class MovedEquipment()
|
||||
: EventWrapper<(EquipSlot, uint, StainIds)[], MovedEquipment.Priority>(nameof(MovedEquipment))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnMovedEquipment"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
|
||||
public MovedEquipment()
|
||||
: base(nameof(MovedEquipment))
|
||||
{ }
|
||||
|
||||
public void Invoke((EquipSlot, uint, StainId)[] items)
|
||||
=> Invoke(this, items);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
|
@ -11,7 +10,8 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is the timestamp of the unlock. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class ObjectUnlocked : EventWrapper<Action<ObjectUnlocked.Type, uint, DateTimeOffset>, ObjectUnlocked.Priority>
|
||||
public sealed class ObjectUnlocked()
|
||||
: EventWrapper<ObjectUnlocked.Type, uint, DateTimeOffset, ObjectUnlocked.Priority>(nameof(ObjectUnlocked))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
|
|
@ -25,11 +25,4 @@ public sealed class ObjectUnlocked : EventWrapper<Action<ObjectUnlocked.Type, ui
|
|||
/// <remarks> Currently used as a hack to make the unlock table dirty in it. If anything else starts using this, rework. </remarks>
|
||||
UnlockTable = 0,
|
||||
}
|
||||
|
||||
public ObjectUnlocked()
|
||||
: base(nameof(ObjectUnlocked))
|
||||
{ }
|
||||
|
||||
public void Invoke(Type type, uint id, DateTimeOffset timestamp)
|
||||
=> Invoke(this, type, id, timestamp);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
|
@ -6,18 +5,18 @@ namespace Glamourer.Events;
|
|||
/// <summary>
|
||||
/// Triggered when Penumbra is reloaded.
|
||||
/// </summary>
|
||||
public sealed class PenumbraReloaded : EventWrapper<Action, PenumbraReloaded.Priority>
|
||||
public sealed class PenumbraReloaded()
|
||||
: EventWrapper<PenumbraReloaded.Priority>(nameof(PenumbraReloaded))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Interop.ChangeCustomizeService.Restore"/>
|
||||
ChangeCustomizeService = 0,
|
||||
|
||||
/// <seealso cref="Interop.VisorService.Restore"/>
|
||||
VisorService = 0,
|
||||
|
||||
/// <seealso cref="Interop.VieraEarService.Restore"/>
|
||||
VieraEarService = 0,
|
||||
}
|
||||
|
||||
public PenumbraReloaded()
|
||||
: base(nameof(PenumbraReloaded))
|
||||
{ }
|
||||
|
||||
public void Invoke()
|
||||
=> Invoke(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
using System;
|
||||
using Glamourer.Interop.Structs;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when a model flags an equipment slot for an update.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the model with a flagged slot. </item>
|
||||
/// <item>Parameter is the equipment slot changed. </item>
|
||||
/// <item>Parameter is the model values to change the equipment piece to. </item>
|
||||
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class SlotUpdating : EventWrapper<Action<Model, EquipSlot, Ref<CharacterArmor>, Ref<ulong>>, SlotUpdating.Priority>
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnSlotUpdating"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
|
||||
public SlotUpdating()
|
||||
: base(nameof(SlotUpdating))
|
||||
{ }
|
||||
|
||||
public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue)
|
||||
{
|
||||
var value = new Ref<CharacterArmor>(armor);
|
||||
var @return = new Ref<ulong>(returnValue);
|
||||
Invoke(this, model, slot, value, @return);
|
||||
armor = value;
|
||||
returnValue = @return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
using System;
|
||||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Designs.History;
|
||||
using Glamourer.Interop.Structs;
|
||||
using Glamourer.State;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
|
|
@ -15,55 +16,18 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is any additional data depending on the type of change. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class StateChanged : EventWrapper<Action<StateChanged.Type, StateChanged.Source, ActorState, ActorData, object?>, StateChanged.Priority>
|
||||
public sealed class StateChanged()
|
||||
: EventWrapper<StateChangeType, StateSource, ActorState, ActorData, ITransaction?, StateChanged.Priority>(nameof(StateChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
/// <summary> A characters saved state had the model id changed. This means everything may have changed. Data is the old model id and the new model id. [(uint, uint)] </summary>
|
||||
Model,
|
||||
|
||||
/// <summary> A characters saved state had multiple customization values changed. TData is the old customize array and the applied changes. [(Customize, CustomizeFlag)] </summary>
|
||||
EntireCustomize,
|
||||
|
||||
/// <summary> A characters saved state had a customization value changed. Data is the old value, the new value and the type. [(CustomizeValue, CustomizeValue, CustomizeIndex)]. </summary>
|
||||
Customize,
|
||||
|
||||
/// <summary> A characters saved state had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. </summary>
|
||||
Equip,
|
||||
|
||||
/// <summary> A characters saved state had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. </summary>
|
||||
Weapon,
|
||||
|
||||
/// <summary> A characters saved state had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
|
||||
Stain,
|
||||
|
||||
/// <summary> A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] </summary>
|
||||
Design,
|
||||
|
||||
/// <summary> A characters saved state had its state reset to its game values. This means everything may have changed. Data is null. </summary>
|
||||
Reset,
|
||||
|
||||
/// <summary> A characters saved state had a meta toggle changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
|
||||
Other,
|
||||
}
|
||||
|
||||
public enum Source : byte
|
||||
{
|
||||
Game,
|
||||
Manual,
|
||||
Fixed,
|
||||
Ipc,
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.StateApi.OnStateChanged" />
|
||||
GlamourerIpc = int.MinValue,
|
||||
|
||||
/// <seealso cref="Interop.Penumbra.PenumbraAutoRedraw.OnStateChanged" />
|
||||
PenumbraAutoRedraw = 0,
|
||||
|
||||
/// <seealso cref="EditorHistory.OnStateChanged" />
|
||||
EditorHistory = -1000,
|
||||
}
|
||||
|
||||
public StateChanged()
|
||||
: base(nameof(StateChanged))
|
||||
{ }
|
||||
|
||||
public void Invoke(Type type, Source source, ActorState state, ActorData actors, object? data = null)
|
||||
=> Invoke(this, type, source, state, actors, data);
|
||||
}
|
||||
|
|
|
|||
24
Glamourer/Events/StateFinalized.cs
Normal file
24
Glamourer/Events/StateFinalized.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using Glamourer.Api;
|
||||
using Glamourer.Api.Enums;
|
||||
using Glamourer.Interop.Structs;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when a set of grouped changes finishes being applied to a Glamourer state.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the operation that finished updating the saved state. </item>
|
||||
/// <item>Parameter is the existing actors using this saved state. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class StateFinalized()
|
||||
: EventWrapper<StateFinalizationType, ActorData, StateFinalized.Priority>(nameof(StateFinalized))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="StateApi.OnStateFinalized"/>
|
||||
StateApi = int.MinValue,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Gui;
|
||||
using OtterGui.Classes;
|
||||
|
||||
|
|
@ -12,8 +11,8 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is the design to select if the tab is the designs tab. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class TabSelected : EventWrapper<Action<MainWindow.TabType, Design?>,
|
||||
TabSelected.Priority>
|
||||
public sealed class TabSelected()
|
||||
: EventWrapper<MainWindow.TabType, Design?, TabSelected.Priority>(nameof(TabSelected))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
@ -23,11 +22,4 @@ public sealed class TabSelected : EventWrapper<Action<MainWindow.TabType, Design
|
|||
/// <seealso cref="Gui.MainWindow.OnTabSelected"/>
|
||||
MainWindow = 1,
|
||||
}
|
||||
|
||||
public TabSelected()
|
||||
: base(nameof(TabSelected))
|
||||
{ }
|
||||
|
||||
public void Invoke(MainWindow.TabType type, Design? design)
|
||||
=> Invoke(this, type, design);
|
||||
}
|
||||
|
|
|
|||
22
Glamourer/Events/VieraEarStateChanged.cs
Normal file
22
Glamourer/Events/VieraEarStateChanged.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the state of viera ear visibility for any draw object is changed.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the model with a changed viera ear visibility state. </item>
|
||||
/// <item>Parameter is the new state. </item>
|
||||
/// <item>Parameter is whether to call the original function. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class VieraEarStateChanged()
|
||||
: EventWrapperRef2<Actor, bool, VieraEarStateChanged.Priority>(nameof(VieraEarStateChanged))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnVieraEarChange"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using Glamourer.Interop.Structs;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
|
|
@ -12,22 +11,12 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is whether to call the original function. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class VisorStateChanged : EventWrapper<Action<Model, Ref<bool>>, VisorStateChanged.Priority>
|
||||
public sealed class VisorStateChanged()
|
||||
: EventWrapperRef3<Model, bool, bool, VisorStateChanged.Priority>(nameof(VisorStateChanged))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnVisorChange"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
|
||||
public VisorStateChanged()
|
||||
: base(nameof(VisorStateChanged))
|
||||
{ }
|
||||
|
||||
public void Invoke(Model model, ref bool state)
|
||||
{
|
||||
var value = new Ref<bool>(state);
|
||||
Invoke(this, model, value);
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using Glamourer.Interop.Structs;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
|
@ -14,7 +13,8 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is the model values to change the weapon to. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class WeaponLoading : EventWrapper<Action<Actor, EquipSlot, Ref<CharacterWeapon>>, WeaponLoading.Priority>
|
||||
public sealed class WeaponLoading()
|
||||
: EventWrapperRef3<Actor, EquipSlot, CharacterWeapon, WeaponLoading.Priority>(nameof(WeaponLoading))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
@ -24,15 +24,4 @@ public sealed class WeaponLoading : EventWrapper<Action<Actor, EquipSlot, Ref<Ch
|
|||
/// <seealso cref="Automation.AutoDesignApplier.OnWeaponLoading"/>
|
||||
AutoDesignApplier = -1,
|
||||
}
|
||||
|
||||
public WeaponLoading()
|
||||
: base(nameof(WeaponLoading))
|
||||
{ }
|
||||
|
||||
public void Invoke(Actor actor, EquipSlot slot, ref CharacterWeapon weapon)
|
||||
{
|
||||
var value = new Ref<CharacterWeapon>(weapon);
|
||||
Invoke(this, actor, slot, value);
|
||||
weapon = value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using Glamourer.Interop.Structs;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Glamourer.Events;
|
||||
|
||||
|
|
@ -11,22 +10,11 @@ namespace Glamourer.Events;
|
|||
/// <item>Parameter is the new state. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class WeaponVisibilityChanged : EventWrapper<Action<Actor, Ref<bool>>, WeaponVisibilityChanged.Priority>
|
||||
public sealed class WeaponVisibilityChanged() : EventWrapperRef2<Actor, bool, WeaponVisibilityChanged.Priority>(nameof(WeaponVisibilityChanged))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="State.StateListener.OnWeaponVisibilityChange"/>
|
||||
StateListener = 0,
|
||||
}
|
||||
|
||||
public WeaponVisibilityChanged()
|
||||
: base(nameof(WeaponVisibilityChanged))
|
||||
{ }
|
||||
|
||||
public void Invoke(Actor actor, ref bool state)
|
||||
{
|
||||
var value = new Ref<bool>(state);
|
||||
Invoke(this, actor, value);
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
54
Glamourer/GameData/ColorParameters.cs
Normal file
54
Glamourer/GameData/ColorParameters.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using Penumbra.String.Functions;
|
||||
|
||||
namespace Glamourer.GameData;
|
||||
|
||||
/// <summary> Parse the Human.cmp file as a list of 4-byte integer values to obtain colors. </summary>
|
||||
public class ColorParameters : IReadOnlyList<uint>
|
||||
{
|
||||
private readonly uint[] _rgbaColors;
|
||||
|
||||
/// <summary> Get a slice of the colors starting at <paramref name="offset"/> and containing <paramref name="count"/> colors. </summary>
|
||||
public ReadOnlySpan<uint> GetSlice(int offset, int count)
|
||||
=> _rgbaColors.AsSpan(offset, count);
|
||||
|
||||
public unsafe ColorParameters(IDataManager gameData, IPluginLog log)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = gameData.GetFile("chara/xls/charamake/human.cmp")!;
|
||||
// Just copy all the data into an uint array.
|
||||
_rgbaColors = new uint[file.Data.Length >> 2];
|
||||
fixed (byte* ptr1 = file.Data)
|
||||
{
|
||||
fixed (uint* ptr2 = _rgbaColors)
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked(ptr2, ptr1, file.Data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
|
||||
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
|
||||
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
|
||||
_rgbaColors = [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<uint> GetEnumerator()
|
||||
=> (IEnumerator<uint>)_rgbaColors.GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count
|
||||
=> _rgbaColors.Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint this[int index]
|
||||
=> _rgbaColors[index];
|
||||
}
|
||||
|
|
@ -1,28 +1,36 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
namespace Glamourer.GameData;
|
||||
|
||||
// Any customization value can be represented in 8 bytes by its ID,
|
||||
// a byte value, an optional value-id and an optional icon or color.
|
||||
/// <summary>
|
||||
/// Any customization value can be represented in 8 bytes by its ID,
|
||||
/// a byte value, an optional value-id and an optional icon or color.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public readonly struct CustomizeData : IEquatable<CustomizeData>
|
||||
{
|
||||
/// <summary> The index of the option this value is for. </summary>
|
||||
[FieldOffset(0)]
|
||||
public readonly CustomizeIndex Index;
|
||||
|
||||
/// <summary> The value for the option. </summary>
|
||||
[FieldOffset(1)]
|
||||
public readonly CustomizeValue Value;
|
||||
|
||||
/// <summary> The internal ID for sheets. </summary>
|
||||
[FieldOffset(2)]
|
||||
public readonly ushort CustomizeId;
|
||||
|
||||
/// <summary> An ID for an associated icon. </summary>
|
||||
[FieldOffset(4)]
|
||||
public readonly uint IconId;
|
||||
|
||||
/// <summary> An ID for an associated color. </summary>
|
||||
[FieldOffset(4)]
|
||||
public readonly uint Color;
|
||||
|
||||
/// <summary> Construct a CustomizeData from single data values. </summary>
|
||||
public CustomizeData(CustomizeIndex index, CustomizeValue value, uint data = 0, ushort customizeId = 0)
|
||||
{
|
||||
Index = index;
|
||||
|
|
@ -32,14 +40,23 @@ public readonly struct CustomizeData : IEquatable<CustomizeData>
|
|||
CustomizeId = customizeId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(CustomizeData other)
|
||||
=> Index == other.Index
|
||||
&& Value.Value == other.Value.Value
|
||||
&& CustomizeId == other.CustomizeId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is CustomizeData other && Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine((int)Index, Value.Value, CustomizeId);
|
||||
|
||||
public static bool operator ==(CustomizeData left, CustomizeData right)
|
||||
=> left.Equals(right);
|
||||
|
||||
public static bool operator !=(CustomizeData left, CustomizeData right)
|
||||
=> !(left == right);
|
||||
}
|
||||
96
Glamourer/GameData/CustomizeManager.cs
Normal file
96
Glamourer/GameData/CustomizeManager.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Race = Penumbra.GameData.Enums.Race;
|
||||
|
||||
namespace Glamourer.GameData;
|
||||
|
||||
/// <summary> Generate everything about customization per tribe and gender. </summary>
|
||||
public class CustomizeManager : IAsyncDataContainer
|
||||
{
|
||||
/// <summary> All races except for Unknown </summary>
|
||||
public static readonly IReadOnlyList<Race> Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
|
||||
|
||||
/// <summary> All tribes except for Unknown </summary>
|
||||
public static readonly IReadOnlyList<SubRace> Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
|
||||
|
||||
/// <summary> Two genders. </summary>
|
||||
public static readonly IReadOnlyList<Gender> Genders =
|
||||
[
|
||||
Gender.Male,
|
||||
Gender.Female,
|
||||
];
|
||||
|
||||
/// <summary> Every tribe and gender has a separate set of available customizations. </summary>
|
||||
public CustomizeSet GetSet(SubRace race, Gender gender)
|
||||
{
|
||||
if (!Finished)
|
||||
Awaiter.Wait();
|
||||
return _customizationSets[ToIndex(race, gender)];
|
||||
}
|
||||
|
||||
/// <summary> Get specific icons. </summary>
|
||||
public ISharedImmediateTexture GetIcon(uint id)
|
||||
=> _icons.TextureProvider.GetFromGameIcon(id);
|
||||
|
||||
/// <summary> Iterate over all supported genders and clans. </summary>
|
||||
public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets()
|
||||
{
|
||||
foreach (var clan in Clans)
|
||||
{
|
||||
yield return (clan, Gender.Male);
|
||||
yield return (clan, Gender.Female);
|
||||
}
|
||||
}
|
||||
|
||||
public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet)
|
||||
{
|
||||
_icons = new TextureCache(gameData, textures);
|
||||
var stopwatch = new Stopwatch();
|
||||
var tmpTask = Task.Run(() =>
|
||||
{
|
||||
stopwatch.Start();
|
||||
return new CustomizeSetFactory(gameData, log, _icons, npcCustomizeSet);
|
||||
});
|
||||
var setTasks = AllSets().Select(p
|
||||
=> tmpTask.ContinueWith(t => _customizationSets[ToIndex(p.Clan, p.Gender)] = t.Result.CreateSet(p.Clan, p.Gender)));
|
||||
Awaiter = Task.WhenAll(setTasks).ContinueWith(_ =>
|
||||
{
|
||||
// This is far too hard to estimate sensibly.
|
||||
TotalCount = 0;
|
||||
Memory = 0;
|
||||
Time = stopwatch.ElapsedMilliseconds;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task Awaiter { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Finished
|
||||
=> Awaiter.IsCompletedSuccessfully;
|
||||
|
||||
private readonly TextureCache _icons;
|
||||
private static readonly int ListSize = Clans.Count * Genders.Count;
|
||||
private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize];
|
||||
|
||||
/// <summary> Get the index for the given pair of tribe and gender. </summary>
|
||||
private static int ToIndex(SubRace race, Gender gender)
|
||||
{
|
||||
var idx = ((int)race - 1) * Genders.Count + (gender == Gender.Female ? 1 : 0);
|
||||
if (idx < 0 || idx >= ListSize)
|
||||
throw new Exception($"Invalid customization requested for {race} {gender}.");
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
public long Time { get; private set; }
|
||||
public long Memory { get; private set; }
|
||||
|
||||
public string Name
|
||||
=> nameof(CustomizeManager);
|
||||
|
||||
public int TotalCount { get; private set; }
|
||||
}
|
||||
303
Glamourer/GameData/CustomizeParameterData.cs
Normal file
303
Glamourer/GameData/CustomizeParameterData.cs
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
using FFXIVClientStructs.FFXIV.Shader;
|
||||
|
||||
namespace Glamourer.GameData;
|
||||
|
||||
public struct CustomizeParameterData
|
||||
{
|
||||
public Vector4 DecalColor;
|
||||
public Vector4 LipDiffuse;
|
||||
public Vector3 SkinDiffuse;
|
||||
public Vector3 SkinSpecular;
|
||||
public Vector3 HairDiffuse;
|
||||
public Vector3 HairSpecular;
|
||||
public Vector3 HairHighlight;
|
||||
public Vector3 LeftEye;
|
||||
public float LeftLimbalIntensity;
|
||||
public Vector3 RightEye;
|
||||
public float RightLimbalIntensity;
|
||||
public Vector3 FeatureColor;
|
||||
public float FacePaintUvMultiplier;
|
||||
public float FacePaintUvOffset;
|
||||
public float MuscleTone;
|
||||
|
||||
public CustomizeParameterValue this[CustomizeParameterFlag flag]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
readonly get
|
||||
{
|
||||
return flag switch
|
||||
{
|
||||
CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(SkinDiffuse),
|
||||
CustomizeParameterFlag.MuscleTone => new CustomizeParameterValue(MuscleTone),
|
||||
CustomizeParameterFlag.SkinSpecular => new CustomizeParameterValue(SkinSpecular),
|
||||
CustomizeParameterFlag.LipDiffuse => new CustomizeParameterValue(LipDiffuse),
|
||||
CustomizeParameterFlag.HairDiffuse => new CustomizeParameterValue(HairDiffuse),
|
||||
CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(HairSpecular),
|
||||
CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(HairHighlight),
|
||||
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye),
|
||||
CustomizeParameterFlag.LeftLimbalIntensity => new CustomizeParameterValue(LeftLimbalIntensity),
|
||||
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye),
|
||||
CustomizeParameterFlag.RightLimbalIntensity => new CustomizeParameterValue(RightLimbalIntensity),
|
||||
CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(FeatureColor),
|
||||
CustomizeParameterFlag.DecalColor => new CustomizeParameterValue(DecalColor),
|
||||
CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(FacePaintUvMultiplier),
|
||||
CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(FacePaintUvOffset),
|
||||
_ => CustomizeParameterValue.Zero,
|
||||
};
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
set => Set(flag, value);
|
||||
}
|
||||
|
||||
public bool Set(CustomizeParameterFlag flag, CustomizeParameterValue value)
|
||||
{
|
||||
return flag switch
|
||||
{
|
||||
CustomizeParameterFlag.SkinDiffuse => SetIfDifferent(ref SkinDiffuse, value.InternalTriple),
|
||||
CustomizeParameterFlag.MuscleTone => SetIfDifferent(ref MuscleTone, value.Single),
|
||||
CustomizeParameterFlag.SkinSpecular => SetIfDifferent(ref SkinSpecular, value.InternalTriple),
|
||||
CustomizeParameterFlag.LipDiffuse => SetIfDifferent(ref LipDiffuse, value.InternalQuadruple),
|
||||
CustomizeParameterFlag.HairDiffuse => SetIfDifferent(ref HairDiffuse, value.InternalTriple),
|
||||
CustomizeParameterFlag.HairSpecular => SetIfDifferent(ref HairSpecular, value.InternalTriple),
|
||||
CustomizeParameterFlag.HairHighlight => SetIfDifferent(ref HairHighlight, value.InternalTriple),
|
||||
CustomizeParameterFlag.LeftEye => SetIfDifferent(ref LeftEye, value.InternalTriple),
|
||||
CustomizeParameterFlag.LeftLimbalIntensity => SetIfDifferent(ref LeftLimbalIntensity, value.Single),
|
||||
CustomizeParameterFlag.RightEye => SetIfDifferent(ref RightEye, value.InternalTriple),
|
||||
CustomizeParameterFlag.RightLimbalIntensity => SetIfDifferent(ref RightLimbalIntensity, value.Single),
|
||||
CustomizeParameterFlag.FeatureColor => SetIfDifferent(ref FeatureColor, value.InternalTriple),
|
||||
CustomizeParameterFlag.DecalColor => SetIfDifferent(ref DecalColor, value.InternalQuadruple),
|
||||
CustomizeParameterFlag.FacePaintUvMultiplier => SetIfDifferent(ref FacePaintUvMultiplier, value.Single),
|
||||
CustomizeParameterFlag.FacePaintUvOffset => SetIfDifferent(ref FacePaintUvOffset, value.Single),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public readonly void Apply(ref CustomizeParameter parameters, CustomizeParameterFlag flags = CustomizeParameterExtensions.All)
|
||||
{
|
||||
parameters.SkinColor = (flags & (CustomizeParameterFlag.SkinDiffuse | CustomizeParameterFlag.MuscleTone)) switch
|
||||
{
|
||||
0 => parameters.SkinColor,
|
||||
CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(SkinDiffuse, parameters.SkinColor.W).XivQuadruple,
|
||||
CustomizeParameterFlag.MuscleTone => parameters.SkinColor with { W = MuscleTone },
|
||||
_ => new CustomizeParameterValue(SkinDiffuse, MuscleTone).XivQuadruple,
|
||||
};
|
||||
|
||||
parameters.LeftColor = (flags & (CustomizeParameterFlag.LeftEye | CustomizeParameterFlag.LeftLimbalIntensity)) switch
|
||||
{
|
||||
0 => parameters.LeftColor,
|
||||
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple,
|
||||
CustomizeParameterFlag.LeftLimbalIntensity => parameters.LeftColor with { W = LeftLimbalIntensity },
|
||||
_ => new CustomizeParameterValue(LeftEye, LeftLimbalIntensity).XivQuadruple,
|
||||
};
|
||||
|
||||
parameters.RightColor = (flags & (CustomizeParameterFlag.RightEye | CustomizeParameterFlag.RightLimbalIntensity)) switch
|
||||
{
|
||||
0 => parameters.RightColor,
|
||||
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple,
|
||||
CustomizeParameterFlag.RightLimbalIntensity => parameters.RightColor with { W = RightLimbalIntensity },
|
||||
_ => new CustomizeParameterValue(RightEye, RightLimbalIntensity).XivQuadruple,
|
||||
};
|
||||
|
||||
if (flags.HasFlag(CustomizeParameterFlag.SkinSpecular))
|
||||
parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple;
|
||||
if (flags.HasFlag(CustomizeParameterFlag.HairDiffuse))
|
||||
{
|
||||
// Vector3 is 0x10 byte for some reason.
|
||||
var triple = new CustomizeParameterValue(HairDiffuse).XivTriple;
|
||||
parameters.MainColor.X = triple.X;
|
||||
parameters.MainColor.Y = triple.Y;
|
||||
parameters.MainColor.Z = triple.Z;
|
||||
}
|
||||
|
||||
if (flags.HasFlag(CustomizeParameterFlag.HairSpecular))
|
||||
parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple;
|
||||
if (flags.HasFlag(CustomizeParameterFlag.HairHighlight))
|
||||
{
|
||||
// Vector3 is 0x10 byte for some reason.
|
||||
var triple = new CustomizeParameterValue(HairHighlight).XivTriple;
|
||||
parameters.MeshColor.X = triple.X;
|
||||
parameters.MeshColor.Y = triple.Y;
|
||||
parameters.MeshColor.Z = triple.Z;
|
||||
}
|
||||
|
||||
if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvMultiplier))
|
||||
GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier;
|
||||
if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvOffset))
|
||||
GetUvOffsetWrite(ref parameters) = FacePaintUvOffset;
|
||||
if (flags.HasFlag(CustomizeParameterFlag.LipDiffuse))
|
||||
parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple;
|
||||
if (flags.HasFlag(CustomizeParameterFlag.FeatureColor))
|
||||
parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public readonly void Apply(ref DecalParameters parameters, CustomizeParameterFlag flags = CustomizeParameterExtensions.All)
|
||||
{
|
||||
if (flags.HasFlag(CustomizeParameterFlag.DecalColor))
|
||||
parameters.Color = new CustomizeParameterValue(DecalColor).XivQuadruple;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public readonly void ApplySingle(ref CustomizeParameter parameters, CustomizeParameterFlag flag)
|
||||
{
|
||||
switch (flag)
|
||||
{
|
||||
case CustomizeParameterFlag.SkinDiffuse:
|
||||
parameters.SkinColor = new CustomizeParameterValue(SkinDiffuse, parameters.SkinColor.W).XivQuadruple;
|
||||
break;
|
||||
case CustomizeParameterFlag.MuscleTone:
|
||||
parameters.SkinColor.W = MuscleTone;
|
||||
break;
|
||||
case CustomizeParameterFlag.SkinSpecular:
|
||||
parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple;
|
||||
break;
|
||||
case CustomizeParameterFlag.LipDiffuse:
|
||||
parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple;
|
||||
break;
|
||||
case CustomizeParameterFlag.HairDiffuse:
|
||||
// Vector3 is 0x10 byte for some reason.
|
||||
var triple1 = new CustomizeParameterValue(HairDiffuse).XivTriple;
|
||||
parameters.MainColor.X = triple1.X;
|
||||
parameters.MainColor.Y = triple1.Y;
|
||||
parameters.MainColor.Z = triple1.Z;
|
||||
break;
|
||||
case CustomizeParameterFlag.HairSpecular:
|
||||
parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple;
|
||||
break;
|
||||
case CustomizeParameterFlag.HairHighlight:
|
||||
// Vector3 is 0x10 byte for some reason.
|
||||
var triple2 = new CustomizeParameterValue(HairHighlight).XivTriple;
|
||||
parameters.MeshColor.X = triple2.X;
|
||||
parameters.MeshColor.Y = triple2.Y;
|
||||
parameters.MeshColor.Z = triple2.Z;
|
||||
break;
|
||||
case CustomizeParameterFlag.LeftEye:
|
||||
parameters.LeftColor = new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple;
|
||||
break;
|
||||
case CustomizeParameterFlag.RightEye:
|
||||
parameters.RightColor = new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple;
|
||||
break;
|
||||
case CustomizeParameterFlag.FeatureColor:
|
||||
parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple;
|
||||
break;
|
||||
case CustomizeParameterFlag.FacePaintUvMultiplier:
|
||||
GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier;
|
||||
break;
|
||||
case CustomizeParameterFlag.FacePaintUvOffset:
|
||||
GetUvOffsetWrite(ref parameters) = FacePaintUvOffset;
|
||||
break;
|
||||
case CustomizeParameterFlag.LeftLimbalIntensity:
|
||||
parameters.LeftColor.W = LeftLimbalIntensity;
|
||||
break;
|
||||
case CustomizeParameterFlag.RightLimbalIntensity:
|
||||
parameters.RightColor.W = RightLimbalIntensity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static CustomizeParameterData FromParameters(in CustomizeParameter parameter, in DecalParameters decal)
|
||||
=> new()
|
||||
{
|
||||
FacePaintUvOffset = GetUvOffset(parameter),
|
||||
FacePaintUvMultiplier = GetUvMultiplier(parameter),
|
||||
MuscleTone = parameter.SkinColor.W,
|
||||
SkinDiffuse = new CustomizeParameterValue(parameter.SkinColor).InternalTriple,
|
||||
SkinSpecular = new CustomizeParameterValue(parameter.SkinFresnelValue0).InternalTriple,
|
||||
LipDiffuse = new CustomizeParameterValue(parameter.LipColor).InternalQuadruple,
|
||||
HairDiffuse = new CustomizeParameterValue(parameter.MainColor).InternalTriple,
|
||||
HairSpecular = new CustomizeParameterValue(parameter.HairFresnelValue0).InternalTriple,
|
||||
HairHighlight = new CustomizeParameterValue(parameter.MeshColor).InternalTriple,
|
||||
LeftEye = new CustomizeParameterValue(parameter.LeftColor).InternalTriple,
|
||||
LeftLimbalIntensity = new CustomizeParameterValue(parameter.LeftColor.W).Single,
|
||||
RightEye = new CustomizeParameterValue(parameter.RightColor).InternalTriple,
|
||||
RightLimbalIntensity = new CustomizeParameterValue(parameter.RightColor.W).Single,
|
||||
FeatureColor = new CustomizeParameterValue(parameter.OptionColor).InternalTriple,
|
||||
DecalColor = FromParameter(decal),
|
||||
};
|
||||
|
||||
public static CustomizeParameterValue FromParameter(in CustomizeParameter parameter, CustomizeParameterFlag flag)
|
||||
=> flag switch
|
||||
{
|
||||
CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(parameter.SkinColor),
|
||||
CustomizeParameterFlag.MuscleTone => new CustomizeParameterValue(parameter.SkinColor.W),
|
||||
CustomizeParameterFlag.SkinSpecular => new CustomizeParameterValue(parameter.SkinFresnelValue0),
|
||||
CustomizeParameterFlag.LipDiffuse => new CustomizeParameterValue(parameter.LipColor),
|
||||
CustomizeParameterFlag.HairDiffuse => new CustomizeParameterValue(parameter.MainColor),
|
||||
CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(parameter.HairFresnelValue0),
|
||||
CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(parameter.MeshColor),
|
||||
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(parameter.LeftColor),
|
||||
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(parameter.RightColor),
|
||||
CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(parameter.OptionColor),
|
||||
CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(GetUvMultiplier(parameter)),
|
||||
CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(GetUvOffset(parameter)),
|
||||
_ => CustomizeParameterValue.Zero,
|
||||
};
|
||||
|
||||
public static Vector4 FromParameter(in DecalParameters parameter)
|
||||
=> new CustomizeParameterValue(parameter.Color).InternalQuadruple;
|
||||
|
||||
private static bool SetIfDifferent(ref Vector3 val, Vector3 @new)
|
||||
{
|
||||
if (@new == val)
|
||||
return false;
|
||||
|
||||
val = @new;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool SetIfDifferent(ref float val, float @new)
|
||||
{
|
||||
if (@new == val)
|
||||
return false;
|
||||
|
||||
val = @new;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool SetIfDifferent(ref Vector4 val, Vector4 @new)
|
||||
{
|
||||
if (@new == val)
|
||||
return false;
|
||||
|
||||
val = @new;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static unsafe float GetUvOffset(in CustomizeParameter parameter)
|
||||
{
|
||||
// TODO CS Update
|
||||
fixed (CustomizeParameter* ptr = ¶meter)
|
||||
{
|
||||
return ((float*)ptr)[23];
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe ref float GetUvOffsetWrite(ref CustomizeParameter parameter)
|
||||
{
|
||||
// TODO CS Update
|
||||
fixed (CustomizeParameter* ptr = ¶meter)
|
||||
{
|
||||
return ref ((float*)ptr)[23];
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe float GetUvMultiplier(in CustomizeParameter parameter)
|
||||
{
|
||||
// TODO CS Update
|
||||
fixed (CustomizeParameter* ptr = ¶meter)
|
||||
{
|
||||
return ((float*)ptr)[15];
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe ref float GetUvMultiplierWrite(ref CustomizeParameter parameter)
|
||||
{
|
||||
// TODO CS Update
|
||||
fixed (CustomizeParameter* ptr = ¶meter)
|
||||
{
|
||||
return ref ((float*)ptr)[15];
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Glamourer/GameData/CustomizeParameterFlag.cs
Normal file
74
Glamourer/GameData/CustomizeParameterFlag.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
namespace Glamourer.GameData;
|
||||
|
||||
[Flags]
|
||||
public enum CustomizeParameterFlag : ushort
|
||||
{
|
||||
SkinDiffuse = 0x0001,
|
||||
MuscleTone = 0x0002,
|
||||
SkinSpecular = 0x0004,
|
||||
LipDiffuse = 0x0008,
|
||||
HairDiffuse = 0x0010,
|
||||
HairSpecular = 0x0020,
|
||||
HairHighlight = 0x0040,
|
||||
LeftEye = 0x0080,
|
||||
RightEye = 0x0100,
|
||||
FeatureColor = 0x0200,
|
||||
FacePaintUvMultiplier = 0x0400,
|
||||
FacePaintUvOffset = 0x0800,
|
||||
DecalColor = 0x1000,
|
||||
LeftLimbalIntensity = 0x2000,
|
||||
RightLimbalIntensity = 0x4000,
|
||||
}
|
||||
|
||||
public static class CustomizeParameterExtensions
|
||||
{
|
||||
// Speculars are not available anymore.
|
||||
public const CustomizeParameterFlag All = (CustomizeParameterFlag)0x7FDB;
|
||||
|
||||
public const CustomizeParameterFlag RgbTriples = All
|
||||
& ~(RgbaQuadruples | Percentages | Values);
|
||||
|
||||
public const CustomizeParameterFlag RgbaQuadruples = CustomizeParameterFlag.DecalColor | CustomizeParameterFlag.LipDiffuse;
|
||||
|
||||
public const CustomizeParameterFlag Percentages = CustomizeParameterFlag.MuscleTone
|
||||
| CustomizeParameterFlag.LeftLimbalIntensity
|
||||
| CustomizeParameterFlag.RightLimbalIntensity;
|
||||
|
||||
public const CustomizeParameterFlag Values = CustomizeParameterFlag.FacePaintUvOffset | CustomizeParameterFlag.FacePaintUvMultiplier;
|
||||
|
||||
public static readonly IReadOnlyList<CustomizeParameterFlag> AllFlags = [.. Enum.GetValues<CustomizeParameterFlag>().Where(f => All.HasFlag(f))];
|
||||
public static readonly IReadOnlyList<CustomizeParameterFlag> RgbaFlags = AllFlags.Where(f => RgbaQuadruples.HasFlag(f)).ToArray();
|
||||
public static readonly IReadOnlyList<CustomizeParameterFlag> RgbFlags = AllFlags.Where(f => RgbTriples.HasFlag(f)).ToArray();
|
||||
public static readonly IReadOnlyList<CustomizeParameterFlag> PercentageFlags = AllFlags.Where(f => Percentages.HasFlag(f)).ToArray();
|
||||
public static readonly IReadOnlyList<CustomizeParameterFlag> ValueFlags = AllFlags.Where(f => Values.HasFlag(f)).ToArray();
|
||||
|
||||
public static int Count(this CustomizeParameterFlag flag)
|
||||
=> RgbaQuadruples.HasFlag(flag) ? 4 : RgbTriples.HasFlag(flag) ? 3 : 1;
|
||||
|
||||
public static IEnumerable<CustomizeParameterFlag> Iterate(this CustomizeParameterFlag flags)
|
||||
=> AllFlags.Where(f => flags.HasFlag(f));
|
||||
|
||||
public static int ToInternalIndex(this CustomizeParameterFlag flag)
|
||||
=> BitOperations.TrailingZeroCount((uint)flag);
|
||||
|
||||
public static string ToName(this CustomizeParameterFlag flag)
|
||||
=> flag switch
|
||||
{
|
||||
CustomizeParameterFlag.SkinDiffuse => "Skin Color",
|
||||
CustomizeParameterFlag.MuscleTone => "Muscle Tone",
|
||||
CustomizeParameterFlag.SkinSpecular => "Skin Shine",
|
||||
CustomizeParameterFlag.LipDiffuse => "Lip Color",
|
||||
CustomizeParameterFlag.HairDiffuse => "Hair Color",
|
||||
CustomizeParameterFlag.HairSpecular => "Hair Shine",
|
||||
CustomizeParameterFlag.HairHighlight => "Hair Highlights",
|
||||
CustomizeParameterFlag.LeftEye => "Left Eye Color",
|
||||
CustomizeParameterFlag.RightEye => "Right Eye Color",
|
||||
CustomizeParameterFlag.FeatureColor => "Feature Color",
|
||||
CustomizeParameterFlag.FacePaintUvMultiplier => "Multiplier for Face Paint",
|
||||
CustomizeParameterFlag.FacePaintUvOffset => "Offset of Face Paint",
|
||||
CustomizeParameterFlag.DecalColor => "Face Paint Color",
|
||||
CustomizeParameterFlag.LeftLimbalIntensity => "Left Limbal Ring Intensity",
|
||||
CustomizeParameterFlag.RightLimbalIntensity => "Right Limbal Ring Intensity",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
72
Glamourer/GameData/CustomizeParameterValue.cs
Normal file
72
Glamourer/GameData/CustomizeParameterValue.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
namespace Glamourer.GameData;
|
||||
|
||||
public readonly struct CustomizeParameterValue
|
||||
{
|
||||
public static readonly CustomizeParameterValue Zero = default;
|
||||
|
||||
private readonly Vector4 _data;
|
||||
|
||||
public CustomizeParameterValue(Vector4 data)
|
||||
=> _data = data;
|
||||
|
||||
public CustomizeParameterValue(Vector3 data, float w = 0)
|
||||
=> _data = new Vector4(data, w);
|
||||
|
||||
public CustomizeParameterValue(FFXIVClientStructs.FFXIV.Common.Math.Vector4 data)
|
||||
=> _data = new Vector4(Root(data.X), Root(data.Y), Root(data.Z), data.W);
|
||||
|
||||
public CustomizeParameterValue(FFXIVClientStructs.FFXIV.Common.Math.Vector3 data)
|
||||
=> _data = new Vector4(Root(data.X), Root(data.Y), Root(data.Z), 0);
|
||||
|
||||
public CustomizeParameterValue(float value, float y = 0, float z = 0, float w = 0)
|
||||
=> _data = new Vector4(value, y, z, w);
|
||||
|
||||
public Vector3 InternalTriple
|
||||
=> new(_data.X, _data.Y, _data.Z);
|
||||
|
||||
public float Single
|
||||
=> _data.X;
|
||||
|
||||
public Vector4 InternalQuadruple
|
||||
=> _data;
|
||||
|
||||
public FFXIVClientStructs.FFXIV.Common.Math.Vector4 XivQuadruple
|
||||
=> new(Square(_data.X), Square(_data.Y), Square(_data.Z), _data.W);
|
||||
|
||||
public FFXIVClientStructs.FFXIV.Common.Math.Vector3 XivTriple
|
||||
=> new(Square(_data.X), Square(_data.Y), Square(_data.Z));
|
||||
|
||||
private static float Square(float x)
|
||||
=> x < 0 ? -x * x : x * x;
|
||||
|
||||
private static float Root(float x)
|
||||
=> x < 0 ? -(float)Math.Sqrt(-x) : (float)Math.Sqrt(x);
|
||||
|
||||
public float this[int idx]
|
||||
=> _data[idx];
|
||||
|
||||
public override string ToString()
|
||||
=> _data.ToString();
|
||||
}
|
||||
|
||||
public static class VectorExtensions
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NearEqual(this Vector3 lhs, Vector3 rhs, float eps = 1e-9f)
|
||||
=> (lhs - rhs).LengthSquared() < eps;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NearEqual(this Vector4 lhs, Vector4 rhs, float eps = 1e-9f)
|
||||
=> (lhs - rhs).LengthSquared() < eps;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NearEqual(this CustomizeParameterValue lhs, CustomizeParameterValue rhs, float eps = 1e-9f)
|
||||
=> NearEqual(lhs.InternalQuadruple, rhs.InternalQuadruple, eps);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NearEqual(this float lhs, float rhs, float eps = 1e-5f)
|
||||
{
|
||||
var diff = lhs - rhs;
|
||||
return diff < 0 ? diff > -eps : diff < eps;
|
||||
}
|
||||
}
|
||||
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