Compare commits

..

821 commits

Author SHA1 Message Date
Ottermandias
f8ca572d38 Fix issue in unlocks tab. 2025-12-26 18:23:23 +01:00
Ottermandias
59c9601a9b Fix issue with material value in history. 2025-12-26 18:23:23 +01:00
Actions User
0d9a0d49ab [CI] Updating repo.json for 1.5.1.7 2025-12-20 15:03:30 +00:00
Ottermandias
96f825e298 Update Penumbra API. 2025-12-20 16:01:00 +01:00
Ottermandias
62c9152d8c Merge branch 'main' of github.com:Ottermandias/Glamourer 2025-12-20 16:00:10 +01:00
Actions User
98b702d6e6 [CI] Updating repo.json for 1.5.1.6 2025-12-19 13:16:42 +00:00
Ottermandias
3c68124b29 Ny 2025-12-19 14:15:14 +01:00
Ottermandias
77f3912bf2 Minor touch up ReapplyState. 2025-12-19 14:01:51 +01:00
Ottermandias
643c83a6f3 Touch up CanUnlock. 2025-12-19 13:56:36 +01:00
Ottermandias
5656d88c94 Merge branch 'refs/heads/Exter-N/penumbra-settings' 2025-12-19 01:29:19 +01:00
Ottermandias
cf87184c92 SDK update. 2025-12-19 01:29:12 +01:00
Ottermandias
598f598e82
Merge pull request #115 from CordeliaMist/ReapplyState
Add ReapplyState & ReapplyStateName
2025-12-19 01:27:19 +01:00
Ottermandias
06c593bbcb
Merge pull request #114 from Bracket416/IsUnlocked
IsUnlocked
2025-12-19 01:27:04 +01:00
Ottermandias
507e1268ac Update SDK. 2025-12-17 18:39:52 +01:00
Cordelia
da14548c43
Merge branch 'Ottermandias:main' into ReapplyState 2025-12-13 09:20:49 -08:00
Actions User
5b6517aae8 [CI] Updating repo.json for 1.5.1.5 2025-11-28 22:09:08 +00:00
Ottermandias
aadcf771e7 1.5.1.5 2025-11-28 23:06:53 +01:00
Exter-N
04fb37d661 Display relevant settings in Penumbra 2025-11-13 20:09:26 +01:00
Ottermandias
bf4673a1d9 Save Remove and Inherit for mod associations. 2025-11-07 23:30:18 +01:00
Ottermandias
76b214c643 Fix try-on interaction with Penumbra for facewear. 2025-11-07 23:30:18 +01:00
Ottermandias
434a5a809e Make old backup files overwrite instead of throwing. 2025-11-07 23:30:18 +01:00
Actions User
88fe25f69e [CI] Updating repo.json for testing_1.5.1.4 2025-10-23 15:40:25 +00:00
Ottermandias
bef1e39ac3 Update Libraries. 2025-10-23 17:37:27 +02:00
Ottermandias
c604d5dbe5 Add DeletePlayerState. 2025-10-23 17:25:59 +02:00
Ottermandias
a0d912a395 Fix issue with reverting state of unavailable actors. 2025-10-23 17:25:59 +02:00
Actions User
a56852f918 [CI] Updating repo.json for 1.5.1.3 2025-10-07 11:02:02 +00:00
Ottermandias
4228fc1b89 fu 2025-10-07 12:59:39 +02:00
Ottermandias
e644b8da28 Fix span issue. 2025-10-07 12:53:55 +02:00
Ottermandias
ace3a8f755 Again. 2025-10-07 12:43:40 +02:00
Ottermandias
76ed347cbf Update signatures. 2025-10-07 12:28:18 +02:00
Cordelia Mist
48bef12555 Optional Addition: Include IPC to get the current state of Auto-Reload Gear, and an IPC Event call for when the option changes.
- Should help clear up ambiguity with any external plugins intending to call ReapplyState on a mod-change to themselves, to know if Glamourer has it handled for them.
2025-10-05 10:31:41 -07:00
Ottermandias
c3469a1687 Fix facewear advanced dyes, fix backup service not running in task, update libraries. 2025-09-28 23:55:44 +02:00
Cordelia Mist
d6c36ca4f7 Add IPC Calls to IPC Tester. 2025-09-26 19:12:02 -07:00
Cordelia Mist
44345b9429 Init providers from API 2025-09-26 19:11:39 -07:00
Cordelia Mist
20914bc064 Add ReapplyState & ReapplyStateName with Helpers. 2025-09-26 19:11:07 -07:00
Ottermandias
0a9693daea
Update CodeService.cs 2025-09-15 20:29:13 +02:00
Bracket
8f362c5121
Update StateApi.cs 2025-09-02 02:02:52 +01:00
Bracket
0442fb7b60
Update IpcProviders.cs 2025-09-02 02:02:08 +01:00
Bracket
c62c3c4eea
Update .gitmodules 2025-09-01 11:27:35 +01:00
Bracket
6a34d41f6a
Update .gitmodules 2025-09-01 11:27:11 +01:00
Bracket
c31f6c19a6
Update StateApi.cs 2025-08-31 23:06:54 +01:00
Bracket
da1db70635
Update IpcProviders.cs 2025-08-31 23:06:23 +01:00
Bracket
c420b1f180
Update .gitmodules 2025-08-31 23:04:41 +01:00
Actions User
414bd8bee7 [CI] Updating repo.json for 1.5.1.2 2025-08-28 16:52:43 +00:00
Ottermandias
8e1745d67a Once more with feeling 2025-08-28 18:47:57 +02:00
Actions User
889f01a724 [CI] Updating repo.json for 1.5.1.1 2025-08-26 09:58:08 +00:00
Ottermandias
6e62905fa7 Fix staging incompatibility with CS. 2025-08-26 11:55:00 +02:00
Actions User
654787fa0d [CI] Updating repo.json for 1.5.1.0 2025-08-25 08:45:28 +00:00
Ottermandias
835ba23935 1.5.1.0 2025-08-25 10:43:14 +02:00
Ottermandias
389a8781d6 Update library. 2025-08-25 10:39:38 +02:00
Actions User
3eabe591df [CI] Updating repo.json for testing_1.5.0.9 2025-08-24 13:59:02 +00:00
Ottermandias
487d3b9399 Update PCP Service. 2025-08-24 15:49:29 +02:00
Actions User
4d4e4669dd [CI] Updating repo.json for testing_1.5.0.8 2025-08-22 18:34:49 +00:00
Ottermandias
fb065549e9 Add PCP Service. 2025-08-22 20:32:32 +02:00
Ottermandias
2c34154915 Update API. 2025-08-22 20:32:32 +02:00
Actions User
3704051b0f [CI] Updating repo.json for 1.5.0.7 2025-08-17 08:45:55 +00:00
Ottermandias
b2b8f2b6eb Make glamourers visor toggle trigger static visors. (?!?) 2025-08-17 10:43:26 +02:00
Ottermandias
22e6c0655b Add ear state when toggling meta application via button. 2025-08-16 11:59:08 +02:00
Ottermandias
bb2ba0cf11 Add glasses to advanced dye slot combo. 2025-08-16 11:59:08 +02:00
Ottermandias
e854386b23 Update OtterGui 2025-08-16 11:59:08 +02:00
Actions User
49d24df2e7 [CI] Updating repo.json for 1.5.0.6 2025-08-12 12:53:44 +00:00
Ottermandias
c9b291c2f3 Add new parameter to LoadWeapon hook. 2025-08-12 14:47:09 +02:00
Actions User
65f789880d [CI] Updating repo.json for 1.5.0.5 2025-08-12 10:32:03 +00:00
Ottermandias
abf998a727 Update GameData 2025-08-12 12:29:55 +02:00
Ottermandias
4cc191cb25 Add viera ear visibility to application rules. 2025-08-11 20:53:44 +02:00
Ottermandias
dc431c10a5 Add chat command to toggle automation. 2025-08-11 19:59:27 +02:00
Ottermandias
26862ba78f Update ChangedEquipData. 2025-08-11 19:59:27 +02:00
Actions User
e4374337f2 [CI] Updating repo.json for 1.5.0.4 2025-08-09 18:50:50 +00:00
Ottermandias
240c889fff Fix changed equipment access to use new size. 2025-08-09 20:48:46 +02:00
Ottermandias
612cd31c3e Fix some caravans. 2025-08-09 19:02:32 +02:00
Actions User
304b362002 [CI] Updating repo.json for 1.5.0.3 2025-08-09 16:55:27 +00:00
Ottermandias
8f34f197d0 Another try. 2025-08-09 18:53:22 +02:00
Actions User
1c97266a93 [CI] Updating repo.json for 1.5.0.2 2025-08-09 16:41:32 +00:00
Ottermandias
a9caddafd5 Maybe fix design crashes. 2025-08-09 18:39:14 +02:00
Actions User
34bf95dddb [CI] Updating repo.json for 1.5.0.1 2025-08-09 11:03:48 +00:00
Ottermandias
4761b8f584 Need staging again... 2025-08-09 13:01:48 +02:00
Ottermandias
0d94aae732 Fix popups not working early. 2025-08-09 12:11:42 +02:00
Ottermandias
e83f328cdc Fix resizable child. 2025-08-09 11:58:52 +02:00
Ottermandias
52fd29c478 Woops 2025-08-09 11:52:17 +02:00
Ottermandias
a8b79993df Make QDB ignore close hotkey. 2025-08-09 11:47:11 +02:00
Ottermandias
98574558e5 Set Repo API level to 13 and remove stg from future releases. 2025-08-08 23:07:08 +02:00
Actions User
557cbf23ce [CI] Updating repo.json for 1.5.0.0 2025-08-08 21:06:10 +00:00
Ottermandias
56753ae7ba Use staging for release. 2025-08-08 23:03:58 +02:00
Ottermandias
b66df624f7 Update Gamedata. 2025-08-08 23:01:54 +02:00
Ottermandias
be78f1447b 1.5.0.0 2025-08-08 15:56:08 +02:00
Ottermandias
97e32a3cb7 Update GameData. 2025-08-08 15:48:19 +02:00
Ottermandias
4472920536 Move Viera Ears sig to gamedata. 2025-08-08 15:46:24 +02:00
Ottermandias
ac6a726f57 Remaining API13 updates. 2025-08-08 15:39:05 +02:00
Ottermandias
00d550f4fe Add viera ear flags 2025-08-08 15:38:51 +02:00
Ottermandias
0f98fac157 Add auto-locking to design defaults. 2025-08-08 15:36:47 +02:00
Ottermandias
c25f0f72db Update for ottergui. 2025-08-08 15:36:14 +02:00
Karou
72e05e23bc stupid detached head.... 2025-08-07 21:53:01 -04:00
Karou
2c3bed6ba5 Api 13 grunt work 2025-08-07 21:50:23 -04:00
Ottermandias
d6df9885dc Update GameData. 2025-08-02 00:07:38 +02:00
Ottermandias
e7936500e0
Merge pull request #111 from CordeliaMist/Fix-RevertNotFiringFinalize
Correct StateFinalized not invoking in certain areas
2025-07-28 18:00:26 +02:00
Cordelia Mist
4ef4e65d46 Ensure that reverts called by API invoke their StateFinalizationType upon completing a reset state. 2025-07-28 08:43:13 -07:00
Cordelia Mist
40b4a8fd7a Ensure Revert via Command invokes a StateFinalization type for Reset 2025-07-28 08:37:57 -07:00
Actions User
8a1f03c272 [CI] Updating repo.json for 1.4.0.3 2025-07-14 15:12:49 +00:00
Ottermandias
8ed479eddf Fix issue with invalid bonus items. 2025-07-14 17:09:42 +02:00
Ottermandias
c0a278ca2c Make designs update on mousewheel. 2025-06-15 23:34:00 +02:00
Actions User
2e9a7004c6 [CI] Updating repo.json for 1.4.0.2 2025-06-13 15:21:04 +00:00
Ottermandias
75c76a92b9 Optimize design combos and file system. 2025-06-13 17:17:58 +02:00
Ottermandias
282935c6d6 Allow drag & drop of equipment pieces. 2025-06-03 18:40:41 +02:00
Actions User
d7b189b714 [CI] Updating repo.json for 1.4.0.1 2025-05-29 00:57:24 +00:00
Ottermandias
e3da3f356c Merge branch 'main' of github.com:Ottermandias/Glamourer 2025-05-29 02:55:23 +02:00
Ottermandias
66bed4217f Fix staining template reading. 2025-05-29 02:55:20 +02:00
Ottermandias
56bbf6593a Fix button counting. 2025-05-29 02:55:11 +02:00
Actions User
b8e1e7c384 [CI] Updating repo.json for 1.4.0.0 2025-05-28 11:58:09 +00:00
Ottermandias
a0d2c39f45 1.4.0.0 2025-05-28 13:56:06 +02:00
Ottermandias
07df3186c2 Better. 2025-05-27 14:38:04 +02:00
Ottermandias
5b59e74417 Use CS ReadStainingTemplate again. 2025-05-27 12:06:29 +02:00
Ottermandias
b4485f028d Batch some multi-design changes to skip unnecessary computations. 2025-05-23 15:21:14 +02:00
Ottermandias
74674cfa0c Make combos start from preview selection if possible. 2025-05-23 10:47:47 +02:00
Ottermandias
f192c17c9b Allow drag & drop for color buttons. 2025-05-21 17:46:56 +02:00
Ottermandias
aa1ac29182 Make design selector resizable. 2025-05-21 17:16:55 +02:00
Ottermandias
081ac6bf8b Split ResetAdvanced into two parts. 2025-05-21 17:09:41 +02:00
Ottermandias
e4b32343ae Update libraries. 2025-05-21 17:08:17 +02:00
Ottermandias
c93370ec92 Again. 2025-05-09 00:06:19 +02:00
Ottermandias
9abd7f2767 Make Dynamis IPC working. 2025-05-08 23:44:16 +02:00
Ottermandias
8a9877bb01 Add testing Dynamis IPC for debugging. 2025-05-06 00:31:26 +02:00
Ottermandias
c1e1476fa6 Fix some issues with glamourer not searching mods by name. 2025-05-06 00:30:47 +02:00
Ottermandias
b1abbb8e77 Support model id input in weapon combo, support middle-mouse pipette. 2025-05-06 00:30:27 +02:00
Ottermandias
fcb0660def Implement new IPC methods and API 1.6 2025-05-04 00:36:39 +02:00
Ottermandias
a6073e2a42 Update old PR slightly. 2025-05-03 23:36:03 +02:00
Ottermandias
2c87077918
Merge pull request #107 from Diorik/random_no_repeat
Prevent Random Repeats
2025-05-03 23:29:29 +02:00
Ottermandias
4e0a9f62b9
Merge pull request #109 from Caraxi/api-fix
Fix `SetMetaState` and `SetMetaStateName` not being registered
2025-05-02 17:28:10 +02:00
Actions User
39636f5293 [CI] Updating repo.json for 1.3.8.6 2025-05-01 21:04:23 +00:00
Ottermandias
b53124e708 Temporarily use custom address for ReadStainingTemplate. 2025-05-01 23:02:08 +02:00
Ottermandias
9a684c9ff5 Implement GetDesignListExtended. 2025-04-29 23:50:29 +02:00
Ottermandias
c7d1620c1e Update GameData. 2025-04-19 23:09:48 +02:00
Ottermandias
325b54031c Update libraries. 2025-04-19 22:28:06 +02:00
Caraxi
155a9d6266 Fix SetMetaState and SetMetaStateName not being registered 2025-04-15 14:26:30 +09:30
Actions User
4f6fb44f79 [CI] Updating repo.json for 1.3.8.5 2025-04-10 14:42:24 +00:00
Ottermandias
bfce99859f Update GameData. 2025-04-10 16:39:04 +02:00
Ottermandias
6c556d6a61 Update API. 2025-04-10 16:20:15 +02:00
Ottermandias
7ed42005dd Force higher Penumbra API version and use better IPC for cutscene parents and game objects. 2025-04-09 15:11:04 +02:00
Ottermandias
aad978f5f6 Pass actor in GearsetDataLoaded instead of looking it up. 2025-04-08 22:53:59 +02:00
Ottermandias
c0ad4aab51 Update for new ActorObjectManager. 2025-04-05 18:48:39 +02:00
Ottermandias
8fe0ac8195 Fix weapon color set issue. 2025-04-05 15:12:27 +02:00
Ottermandias
46f8818cee Add Incognito Modifier. 2025-04-04 22:35:48 +02:00
Actions User
118f51cc64 [CI] Updating repo.json for 1.3.8.4 2025-04-02 23:07:10 +00:00
Ottermandias
096d82741d Fix previous fix for weapons. 2025-04-03 01:04:58 +02:00
Actions User
b98cb31fd2 [CI] Updating repo.json for 1.3.8.3 2025-04-02 21:47:24 +00:00
Ottermandias
90813ce030 Update GameData. 2025-04-02 23:45:28 +02:00
Ottermandias
95bc52b2bc Check for valid humanity. 2025-04-02 23:45:07 +02:00
Ottermandias
d79e4b5853
Merge pull request #108 from keifufu/fix-linux-build
Fix linux build
2025-03-31 13:03:43 +02:00
keifufu
a40a6905be
newline be gone 2025-03-31 13:02:19 +02:00
keifufu
6b3a64ce14
fix linux build 2025-03-31 13:00:51 +02:00
Actions User
4fca1600ca [CI] Updating repo.json for 1.3.8.2 2025-03-29 17:06:25 +00:00
Ottermandias
b1e65e6f9d Fix some offsets. 2025-03-29 18:04:18 +01:00
Actions User
381b23fe0c [CI] Updating repo.json for 1.3.8.1 2025-03-28 16:29:25 +00:00
Ottermandias
782c4446b2 Update GameData. 2025-03-28 17:25:34 +01:00
Ottermandias
76eaa75d04 Update Penumbra.Api. 2025-03-28 17:23:25 +01:00
Ottermandias
361ed536a3 Fix issue with NPC automation due to missing job detection. 2025-03-28 17:22:57 +01:00
Ottermandias
b1d00e9812 Update GameData. 2025-03-28 15:54:26 +01:00
Ottermandias
b0abf865cb Change build step. 2025-03-28 15:54:10 +01:00
Ottermandias
d398381b52 Revert Dalamud staging on release, and update api level. 2025-03-28 14:17:16 +01:00
Actions User
d75d70bee5 [CI] Updating repo.json for 1.3.8.0 2025-03-28 13:16:18 +00:00
Ottermandias
296f1e90b5 🤷 2025-03-28 14:13:34 +01:00
Ottermandias
9d3dfbbece use staging build for release for now. 2025-03-28 14:05:53 +01:00
Ottermandias
d6d592f099 1.3.8.0 2025-03-28 13:58:19 +01:00
Ottermandias
00cb1b6643 Use CS sig. 2025-03-28 13:52:15 +01:00
Ottermandias
22babad789 Update. 2025-03-27 18:11:08 +01:00
Ottermandias
18ff905746 Add a chat command to clear temporary settings made by Glamourer. 2025-03-11 18:06:22 +01:00
Ottermandias
fd0d761b92 Fix small issue with invisible customizations applying. 2025-03-11 00:58:05 +01:00
Actions User
750d4f9eca [CI] Updating repo.json for 1.3.7.1 2025-03-10 22:59:41 +00:00
Ottermandias
25517525c9 Keep temporary mod settings manually applied by Glamourer when redrawing with automation changes. 2025-03-10 23:55:29 +01:00
Actions User
99a8e1e8c5 [CI] Updating repo.json for 1.3.7.0 2025-03-09 22:17:20 +00:00
Ottermandias
381b2a1b8b 1.3.7.0 2025-03-09 23:14:19 +01:00
Ottermandias
3dee511b9a Update some obsoletes. 2025-03-09 23:13:33 +01:00
Ottermandias
e93c3b7bb8 Update submodules. 2025-03-09 23:12:21 +01:00
Ottermandias
773682838e Make customize buttons not change advanced customization to On by default. 2025-03-09 13:37:27 +01:00
Ottermandias
c9f00c6369 Mark designs containing advanced dyes, mod associations or links in automation. 2025-03-04 16:28:48 +01:00
Ottermandias
2026069ed3 Make Glamourer able to export and import color sets to and from Penumbra, maybe. 2025-03-04 15:34:39 +01:00
Actions User
ae093506c1 [CI] Updating repo.json for testing_1.3.6.6 2025-03-03 23:21:09 +00:00
Ottermandias
87f1b613f9 Add application shortcuts to multi design panel. 2025-03-04 00:18:41 +01:00
Ottermandias
71e15474b2 Add deletion of advanced dyes to multi design selection. 2025-03-03 18:32:52 +01:00
Ottermandias
3fd6108fa1 Add buttons to remove, enable and disable multiple advanced dyes at once in a design. 2025-03-03 18:12:42 +01:00
Ottermandias
b9e4c144c2 Add some buttons to set design application rules to some presets. 2025-03-03 17:55:38 +01:00
Ottermandias
6e685b96d1 Make all panels configurable. 2025-03-03 16:31:15 +01:00
Ottermandias
c96f009fb4 Slightly speed up QDB drawing. 2025-03-03 16:31:15 +01:00
Actions User
3112079776 [CI] Updating repo.json for testing_1.3.6.5 2025-03-02 12:40:58 +00:00
Ottermandias
7015737d88 Meh. 2025-03-02 13:38:58 +01:00
Ottermandias
94f6e870e6 Update Submodules. 2025-03-02 13:33:41 +01:00
Ottermandias
528aae7eee Remove PalettePlusChecker and config options to disable Advanced Customization and Advanced Dyes. 2025-03-02 13:24:51 +01:00
Ottermandias
425c9471fb Highlight materials with advanced dyes even if inactive, allow removing inactive. 2025-02-23 23:19:24 +01:00
Ottermandias
0c8110e15e Highlight existing advanced dyes in state and design. 2025-02-21 00:10:36 +01:00
Actions User
8a7ec45bbf [CI] Updating repo.json for testing_1.3.6.3 2025-02-19 23:19:21 +00:00
Ottermandias
c1f84b4303 Differentiate between temporary settings through manual and automatic application. 2025-02-20 00:16:55 +01:00
Ottermandias
5a9e9513f4 Skip automatic updates from temporary settings when applied by design. 2025-02-20 00:16:55 +01:00
Ottermandias
9e7679e70f Move check for valid model to actor display instead of identifier list. 2025-02-20 00:16:55 +01:00
Actions User
e5b2114ac2 [CI] Updating repo.json for testing_1.3.6.2 2025-02-18 14:13:13 +00:00
Ottermandias
1168460942 Add button to reset glamourer temporary settings to qdb. 2025-02-17 17:40:56 +01:00
Actions User
f7d6e75a9b [CI] Updating repo.json for testing_1.3.6.1 2025-02-13 15:43:08 +00:00
Ottermandias
d56c2db547 Add more mod association and modded utility. 2025-02-13 16:32:23 +01:00
Ottermandias
ab2a3f5bd9 Add new colors. 2025-02-13 16:30:35 +01:00
Ottermandias
4748d1e211 Allow filtered item names. 2025-02-13 16:30:24 +01:00
Diorik
5ca151b675 PreventRandom use WeakReference, reroll rand instead of changing list 2025-02-06 13:29:47 -06:00
Diorik
67fd65d366 Make PreventRandomc figurable, clean up logic
Will no longer hold design reference or make redundant copy of list
2025-02-06 12:49:23 -06:00
Diorik
45981f2fee
Merge branch 'Ottermandias:main' into random_no_repeat 2025-02-06 11:26:07 -06:00
Ottermandias
1df2a46884 Update Submodule Versions. 2025-02-06 17:00:31 +01:00
Actions User
2c7b7d59be [CI] Updating repo.json for 1.3.6.0 2025-02-06 15:55:34 +00:00
Ottermandias
d849506ecd 1.3.6.0 2025-02-06 16:52:59 +01:00
Ottermandias
47f2cfc210
Merge pull request #105 from Diorik/redraw_command
Add "Reset Design" command
2025-02-06 16:38:10 +01:00
Ottermandias
5d1c65cce7 Fix overwriting with current state keeping old advanced dyes. 2025-02-06 16:27:10 +01:00
Diorik
cf308fc118 Prevent repeating random design
Cache the last selected random design and prevent it from being chosen again.
2025-02-04 01:54:34 -06:00
Diorik
87016419c5 Add "Reset Design" command
A new command to reapply automation while resetting the random design regardless of settings.
2025-02-04 00:46:12 -06:00
Ottermandias
da46705b52 Use new settings API. 2025-01-31 16:06:50 +01:00
Ottermandias
9a1cf3d9e6 Better 2025-01-30 22:45:59 +01:00
Ottermandias
8e0908dbf7 Add more settings to multi design panel. 2025-01-30 22:39:57 +01:00
Ottermandias
1f255a98e6 Fix issue with scaling after zone change. 2025-01-30 13:55:28 +01:00
Ottermandias
2a067ef60b Accept all existing facepaints. 2025-01-30 13:36:13 +01:00
Actions User
b3afa2067c [CI] Updating repo.json for testing_1.3.5.5 2025-01-25 13:02:30 +00:00
Ottermandias
630c4dd894 Update Submodules. 2025-01-25 14:00:36 +01:00
Ottermandias
f1fa5c2fa9 Merge branch 'refs/heads/CordeliaMist/PR-SetMetaState' 2025-01-24 23:29:03 +01:00
Ottermandias
f6c9ecad60 Move MetaFlag completely to API, rename, cleanup. 2025-01-24 23:28:23 +01:00
Cordelia Mist
5eb545f62d Namechange: SetMetaState -> SetMeta
Help clear up confusion on if it is in the StateApi section or the ItemsApi section (Which it is currently in)
2025-01-24 13:09:49 -08:00
Cordelia Mist
0f127a557d Introduced locks, if nessisary for change. 2025-01-24 11:44:09 -08:00
Cordelia Mist
611200d311 Added setMetaState by name 2025-01-24 11:43:26 -08:00
Cordelia Mist
0ed4603ba5 Corrected Paramater fields to include ApplyFlags and removed unessisary using. 2025-01-24 11:42:52 -08:00
Cordelia Mist
439849edfa Append SetMetaState with object index. 2025-01-24 11:35:53 -08:00
Cordelia Mist
091aadd4a6 Add Yield return Indices List for setter helper 2025-01-24 11:25:40 -08:00
Cordelia Mist
356b814102 Append conversion for SetMetaFlag -> MetaIndex for help in moving down the API call to the state editor call. 2025-01-24 11:14:50 -08:00
Actions User
cd6a6a462d [CI] Updating repo.json for testing_1.3.5.4 2025-01-24 16:54:22 +00:00
Ottermandias
0123fe1fbd Increment minor API version. 2025-01-24 17:52:06 +01:00
Ottermandias
8b518d3c6f Merge branch 'refs/heads/CordeliaMist/main' 2025-01-24 17:51:11 +01:00
Ottermandias
d9f9937d41 Continue renames, some cleanup. 2025-01-24 17:50:58 +01:00
Ottermandias
7be283ca30 Apply API renames. 2025-01-24 16:50:29 +01:00
Ottermandias
748c324acf Merge branch 'main' into CordeliaMist/main 2025-01-24 15:13:46 +01:00
Ottermandias
2916669319 Change EquipGearsetInternal to CS. 2025-01-24 15:06:29 +01:00
Ottermandias
30468e0b09 Fix checkbox not working. 2025-01-24 15:04:47 +01:00
Ottermandias
70918e5393 Add AutoDesignSet toggle for resetting settings. 2025-01-24 15:04:47 +01:00
Cordelia Mist
c43ce9d978 Updated new functionality and internal gearset to use FFXIVClientStructs member functions and implemented structs, corrected remaining spacing issues. 2025-01-21 10:47:12 -08:00
Cordelia
2e11481276
Merge branch 'Ottermandias:main' into main 2025-01-20 11:33:55 -08:00
Actions User
f1b335e794 [CI] Updating repo.json for testing_1.3.5.3 2025-01-20 16:49:52 +00:00
Ottermandias
96dca5dbe7 Cherry-Pick from ebdfd3f8 to use EquipGearSetInternal. 2025-01-20 17:47:36 +01:00
Ottermandias
4db0822443 Update GameData. 2025-01-20 15:40:14 +01:00
Ottermandias
cdc67a035c Check for ENPC player copies and treat them as transformations. 2025-01-20 15:40:14 +01:00
Ottermandias
8add6e5519 Fix some .net update issues. 2025-01-20 15:40:14 +01:00
Cordelia Mist
ebdfd3f8bc Correct spacing and formatting further to align with main Glamourer branch preferences. 2025-01-19 09:50:34 -08:00
Cordelia Mist
1cd8e5fb7e Corrected comments on StateApi for PR 2025-01-19 09:35:10 -08:00
Cordelia Mist
1d185e9bfe Added Proper Unsubsribe from OnStateUpdated.
Ensures that ReapplyAutomation is called from OnAutomationChange when change occurs.
Reformatted temporary Signature locations and comments to align with the structure of the respective classes.
2025-01-19 09:07:43 -08:00
Cordelia Mist
9c57935a87 removed unessisary usings and corrected gearsetDataLoaded stateListener ref. 2025-01-17 18:52:16 -08:00
Cordelia Mist
8b609e5f05 corrected comments and formatting to reflect Glamourer's main branch. 2025-01-17 18:43:05 -08:00
Cordelia Mist
e1a41b5f3c Implements true endpoints for all glamourer operations, also correctly marks reverts and gearsets. Replaced back excessive logging to maintain with logging formats expected by glamourer. 2025-01-17 18:43:05 -08:00
Cordelia Mist
c605d19510 Progress made. May be successful! 2025-01-17 18:39:55 -08:00
Actions User
8160f420db [CI] Updating repo.json for testing_1.3.5.2 2025-01-15 17:05:57 +00:00
Ottermandias
60a1ee728a Use current temporary settings for mod associations if they exist. 2025-01-14 14:51:36 +01:00
Ottermandias
c83ddf054a Add counts to multi design selection. 2025-01-12 00:04:00 +01:00
Actions User
02bfd17794 [CI] Updating repo.json for 1.3.5.1 2025-01-11 12:49:40 +00:00
Ottermandias
7d490f9cfb Fix designs not resetting temporary settings if they do not have mod associations. 2025-01-11 13:47:39 +01:00
Actions User
c541fd62c4 [CI] Updating repo.json for 1.3.5.0 2025-01-10 19:16:32 +00:00
Ottermandias
2e038350ef 1.3.5.0 2025-01-10 20:13:51 +01:00
Ottermandias
157a5b150b Update Submodules. 2025-01-10 20:01:46 +01:00
Ottermandias
8a87ff16b4 Improve mod associations display. 2025-01-10 20:01:46 +01:00
Ottermandias
3d6d04dde1 Fix bug in automation tab setting gearsets. 2025-01-10 20:01:46 +01:00
Ottermandias
d675cdc804 Improve scaling of advanced dye window. 2025-01-10 20:01:46 +01:00
Actions User
6475c3c65b [CI] Updating repo.json for testing_1.3.4.4 2024-12-31 17:06:22 +00:00
Ottermandias
d57743763b Update OtterGui. 2024-12-31 18:03:56 +01:00
Ottermandias
9b9e356ad1 Update Submodules. 2024-12-31 16:36:05 +01:00
Ottermandias
24452f3c79 Add initial support for setting temporary mod settings. 2024-12-31 16:35:08 +01:00
Ottermandias
e41755ed7e Freeze the application buttons at the top of the panels. 2024-12-31 15:16:14 +01:00
Ottermandias
71e80740f6 Add selection designs to chat commands. 2024-12-31 13:46:10 +01:00
Ottermandias
70cf21cf57 Accept more colors in designs as valid (independently of clan/gender and mixing highlights/hair and both eyes). 2024-12-29 15:48:54 +01:00
Ottermandias
ebd3fb3fbf Update submodules. 2024-12-29 13:38:21 +01:00
Ottermandias
56d014e14f Fix ImGui Assertion. 2024-12-25 22:21:58 +01:00
Ottermandias
664b42eee8 Update GameData. 2024-12-17 18:05:07 +01:00
Ottermandias
a116243900 Maybe fix disabling auto designs. 2024-12-16 17:23:04 +01:00
Ottermandias
33bf5c5392 Update GameData versioning. 2024-12-16 17:23:04 +01:00
Ottermandias
f3eb542940 Add option to always reset random designs. 2024-12-16 17:23:04 +01:00
Ottermandias
b90e68fbaf Fix inverted application of apply flags in IPC 2024-12-16 17:23:04 +01:00
Actions User
c951854d5a [CI] Updating repo.json for 1.3.4.3 2024-12-13 17:14:14 +00:00
Ottermandias
f424857bd3 Again. 2024-12-13 15:42:49 +01:00
Ottermandias
27e9223c81 Again 2024-12-09 21:29:35 +01:00
Ottermandias
31cd812519 Update GameData. 2024-12-09 21:23:33 +01:00
Ottermandias
c9febe2c74 Try to make random designs in automation stick around when redrawing/changing zone. 2024-11-30 22:11:16 +01:00
Actions User
467dc2c22f [CI] Updating repo.json for 1.3.4.2 2024-11-29 16:36:00 +00:00
Ottermandias
4215e0ad44 Add default settings for design configuration. 2024-11-27 23:44:41 +01:00
Ottermandias
533c53fd8d Fix some issues with Crests 2024-11-27 23:44:41 +01:00
Ottermandias
66ed721105 Treat no Automation Set as empty automation set. 2024-11-27 23:44:41 +01:00
Actions User
9e09d64c66 [CI] Updating repo.json for 1.3.4.1 2024-11-25 16:00:32 +00:00
Ottermandias
7f95726cc3 Do not use DX calls to read color tables except in UI. 2024-11-25 16:53:33 +01:00
Ottermandias
22c7e32760 Fix some context menu things. 2024-11-24 12:18:04 +01:00
Ottermandias
40a684ff46 Fix CalculateHeight. 2024-11-23 13:58:24 +01:00
Ottermandias
9ed8c9517b
Update repo.json 2024-11-22 14:44:29 +01:00
Actions User
3722df199b [CI] Updating repo.json for 1.3.4.0 2024-11-22 13:29:49 +00:00
Ottermandias
5464fa3a0f 1.3.4.0 2024-11-22 14:27:39 +01:00
Ottermandias
525f65e70a Fix issues with shared weapon types. 2024-11-22 14:27:39 +01:00
Actions User
b44a147fdd [CI] Updating repo.json for testing_1.3.3.3 2024-11-20 13:01:59 +00:00
Ottermandias
86d370226a Update weapon changed data offset. 2024-11-20 11:50:44 +01:00
Actions User
f9c7e567b6 [CI] Updating repo.json for testing_1.3.3.2 2024-11-17 23:31:26 +00:00
Ottermandias
7605c7cb29 Fix screen actor indices. 2024-11-18 00:29:31 +01:00
Actions User
0ba9f538ba [CI] Updating repo.json for testing_1.3.3.1 2024-11-17 21:04:05 +00:00
Ottermandias
de9d605fb0 1.3.3.1 2024-11-17 22:01:36 +01:00
Ottermandias
db34255127 Fix customization bug. 2024-11-17 22:00:50 +01:00
Ottermandias
60c7debb5e Fix offset 2024-11-17 18:13:20 +01:00
Ottermandias
c7b9791a6a Not yet increment API level. 2024-11-17 14:29:11 +01:00
Ottermandias
fe028e5ec7 Fixes. 2024-11-17 14:27:14 +01:00
Ottermandias
2ce8076e9a Current State. 2024-11-17 00:51:57 +01:00
Ottermandias
a5998b84ba Fix issues with ResetAdvancedDyes and weapon types. 2024-11-08 10:20:36 +01:00
Actions User
dd217a2475 [CI] Updating repo.json for 1.3.3.0 2024-10-18 14:26:43 +00:00
Ottermandias
4738830b8a 1.3.3.0 2024-10-18 16:02:30 +02:00
Ottermandias
c49102959f Add tails and Height 255 to available NPC options. 2024-10-18 15:34:50 +02:00
Ottermandias
1d974a0c6c
Merge pull request #101 from anya-hichu/main
Fix inverted log and table width to not hide the final "s" from "Reset Advanced Dyes"
2024-10-14 20:40:44 +02:00
Anya
dee79c121b
Merge branch 'Ottermandias:main' into main 2024-10-14 20:39:10 +02:00
Actions User
667ff2490d [CI] Updating repo.json for testing_1.3.2.2 2024-10-13 12:42:47 +00:00
Ottermandias
87d8876972 Fix some bonus item related things. 2024-10-13 14:40:43 +02:00
Actions User
530166b81a [CI] Updating repo.json for testing_1.3.2.1 2024-10-12 13:16:00 +00:00
Ottermandias
6db4a9623b Update GameData. 2024-10-12 15:14:03 +02:00
anya-hichu
11111817d7 Fix DesignInfoTable width 2024-10-12 15:02:55 +02:00
anya-hichu
da944b50cc Fix inverted log 2024-10-12 14:48:57 +02:00
Ottermandias
cca2bf645f Fix PR. 2024-10-12 13:02:30 +02:00
Ottermandias
ef96dadba5
Merge pull request #100 from anya-hichu/main
Add ResetMaterials option to Design
2024-10-12 12:03:20 +02:00
anya-hichu
210bca4c7c Add ResetMaterials option to Design 2024-10-11 19:55:05 +02:00
Ottermandias
9d99d936aa Remove BonusItem and replace with normal EquipItems everywhere. 2024-10-11 18:18:33 +02:00
Ottermandias
415ac63767 Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-10-07 18:35:22 +02:00
Ottermandias
8f53253bae Add special filters to actor selector. 2024-10-07 18:35:09 +02:00
Ottermandias
d104b794ae Add owned NPC button for automation identifiers. 2024-10-07 18:34:52 +02:00
Actions User
ea8b23d3fd [CI] Updating repo.json for 1.3.2.0 2024-10-06 13:05:46 +00:00
Ottermandias
65e33d91ac 1.3.2.0 2024-10-06 15:03:58 +02:00
Ottermandias
ce02c64655 Update Gamedata. 2024-10-06 13:01:28 +02:00
Ottermandias
83e1476e7f Make unlocks tab non-docking. 2024-10-06 12:59:15 +02:00
Ottermandias
885063d389 Make item combos search for entire model string. 2024-10-06 12:59:06 +02:00
Ottermandias
816e88e0bd Make the advanced dye window non-docking. 2024-10-06 12:51:35 +02:00
Ottermandias
726eb52e4f Update Gamedata. 2024-10-06 12:46:50 +02:00
Ottermandias
5da0738147 Add a debug rider for actors. 2024-10-06 12:45:58 +02:00
Ottermandias
9b5271bca4 Update Gamedata. 2024-10-06 11:58:39 +02:00
Ottermandias
c8cc375d4b
Update WorldSets.cs 2024-10-05 02:15:23 +02:00
Ottermandias
be9c3eab40 Add bonus item import from .chara files, fix small bugs when applying designs. 2024-09-21 21:17:24 +02:00
Ottermandias
1f1b04bdfe Update actions. 2024-09-16 23:34:29 +02:00
Ottermandias
f473abb99c Fix chat log context menu try-on. 2024-09-16 22:53:35 +02:00
Ottermandias
9a02ba2987 Add support for previewing non-existing items from Penumbra. 2024-08-31 20:51:38 +02:00
Ottermandias
03043ba2c9 Fix reversion. 2024-08-24 20:40:26 +02:00
Ottermandias
82536c75c9 Fix fun module RNG. 2024-08-24 20:40:02 +02:00
Ottermandias
773f526fba Improve handling for bonus items in designs. 2024-08-15 17:23:51 +02:00
Ottermandias
fc6604fd5a Remove Artisan code. 2024-08-15 15:58:04 +02:00
Ottermandias
a7c5d513d4 Maybe fix weapon hidden state when leaving gpose or changing zones. 2024-08-13 15:27:55 +02:00
Actions User
38a489d7f0 [CI] Updating repo.json for 1.3.1.1 2024-08-10 10:27:38 +00:00
Ottermandias
76cdacf80f Update non-testing Dalamud API Level 2024-08-10 12:24:29 +02:00
Actions User
3880c1870b [CI] Updating repo.json for 1.3.1.0 2024-08-10 10:02:21 +00:00
Ottermandias
6a46a410f7 Update Penumbra API, increment API version. 2024-08-10 11:57:44 +02:00
Ottermandias
5971592217 Add BonusItem API. 2024-08-10 11:52:57 +02:00
Ottermandias
9e06125092 Update GameData. 2024-08-10 00:57:10 +02:00
Ottermandias
3a07acbd05 1.3.1.0 2024-08-09 23:50:59 +02:00
Ottermandias
edc54203b5 Fix display and initial values of design color rows. 2024-08-09 22:09:01 +02:00
Ottermandias
af58a52a59 Update display of saved material values. 2024-08-09 16:27:18 +02:00
Actions User
2282f3f87a [CI] Updating repo.json for testing_1.3.0.11 2024-08-09 14:04:22 +00:00
Ottermandias
5cd224b164 Add design migration for inverted gloss and specular, and a backup before doing that. 2024-08-09 16:01:51 +02:00
Ottermandias
d7074c5791 Improve Gloss/Specular display 2024-08-09 16:01:51 +02:00
Actions User
d594082165 [CI] Updating repo.json for testing_1.3.0.10 2024-08-07 15:59:50 +00:00
Ottermandias
1c8d01bdd3 For later. 2024-08-07 17:57:49 +02:00
Ottermandias
a1b455d9a5 Update advanced dyes. 2024-08-07 17:55:23 +02:00
Ottermandias
61cb46a298 Update for GameData update. 2024-08-06 18:25:00 +02:00
Ottermandias
2e52030c31 Fix issue with adding mods from clipboard 2024-08-06 17:39:34 +02:00
Ottermandias
5bca4b9118 Do not apply Nothing-shield for swords periphery. 2024-08-06 17:38:53 +02:00
Ottermandias
1cc7c2f0cd Add editable mod state and priority. 2024-08-05 14:48:43 +02:00
Ottermandias
6115d24775 Some changes for codes. 2024-08-04 14:36:00 +02:00
Ottermandias
68bffba3cd Update for submodules. 2024-08-04 14:14:10 +02:00
Ottermandias
e2ffcea0b2 Fix some warnings. 2024-08-04 12:40:00 +02:00
Ottermandias
9f04ee7695 Fix visor state toggle. 2024-08-04 12:39:01 +02:00
Ottermandias
f69915dcb3 Remove some warnings. 2024-08-04 12:38:38 +02:00
Ottermandias
34caf29b32 Add more Corgis to Glamourer. 2024-08-04 12:38:04 +02:00
Ottermandias
3a63a1b22d Add some codes. 2024-08-03 02:25:53 +02:00
Ottermandias
5460ffd0a0 Update world sets. 2024-08-02 17:02:40 +02:00
Actions User
e516fd9318 [CI] Updating repo.json for testing_1.3.0.9 2024-08-01 15:08:33 +00:00
Ottermandias
ad79d9cc07 Set focus on detached advanced dyes when opening or toggling with buttons. 2024-08-01 17:06:10 +02:00
Ottermandias
ab771fd010 Add Undo to design panel. 2024-07-31 20:57:50 +02:00
Ottermandias
d8085dc022 Introduce history. 2024-07-31 19:33:23 +02:00
Ottermandias
f6434cbc61 Rename stains. 2024-07-31 17:06:30 +02:00
Ottermandias
9d569266b5 Improve job filter in unlocks tab. 2024-07-31 16:58:33 +02:00
Ottermandias
6446309bd7 Allow drag&drop on stains. 2024-07-30 20:24:05 +02:00
Actions User
d0d518ddc2 [CI] Updating repo.json for testing_1.3.0.8 2024-07-28 12:14:04 +00:00
Ottermandias
e87b216541 Rename material stuff. 2024-07-28 14:11:56 +02:00
Ottermandias
1e0b7fdfce Improve respecting of design write protection. 2024-07-28 01:33:37 +02:00
Ottermandias
1bee5c680b Add Facewear to application rules. 2024-07-27 13:59:39 +02:00
Actions User
029cf12bed [CI] Updating repo.json for testing_1.3.0.7 2024-07-26 13:37:25 +00:00
Ottermandias
ff96e51963 Maybe fix another issue with monk offhands. 2024-07-26 15:35:25 +02:00
Ottermandias
60302c37cd Fix minion placement. 2024-07-26 15:06:51 +02:00
Ottermandias
fb2b676ff0 Remove no longer needed helpers. 2024-07-26 15:06:29 +02:00
Actions User
d256702005 [CI] Updating repo.json for testing_1.3.0.6 2024-07-22 14:06:34 +00:00
Ottermandias
b2bb50b3d9 Maybe fix an issue with monk fist weapons again. 2024-07-22 16:04:23 +02:00
Actions User
b1c1cf0f99 [CI] Updating repo.json for testing_1.3.0.5 2024-07-20 21:44:33 +00:00
Ottermandias
a7f36da3f5 Fix statesource overwriting data for characterweapon. 2024-07-20 23:42:29 +02:00
Ottermandias
94b7ea2d9d Fix changed data offset for weapon. 2024-07-20 23:42:04 +02:00
Ottermandias
55f2053fe6 Fix BodyType not applying. 2024-07-20 22:01:21 +02:00
Ottermandias
a885411a8c Fix saving of favorites. 2024-07-20 21:47:00 +02:00
Ottermandias
3ad67f661a Add Hrothgar face hacks to race changing fixing of values. 2024-07-20 21:41:49 +02:00
Actions User
ed329ec989 [CI] Updating repo.json for testing_1.3.0.4 2024-07-19 15:30:58 +00:00
Ottermandias
f669616616 Update GameData. 2024-07-19 17:28:57 +02:00
Ottermandias
2f95a4ea34 Update Advanced Dyes. 2024-07-19 17:27:30 +02:00
Actions User
de9fb1fd9f [CI] Updating repo.json for testing_1.3.0.3 2024-07-17 16:41:59 +00:00
Ottermandias
8b10b3fdfb Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-07-17 18:35:48 +02:00
Ottermandias
dae3fbc901 Update single-setter for 0x10 Vector3, too. 2024-07-17 18:35:34 +02:00
Actions User
608ab7beb9 [CI] Updating repo.json for testing_1.3.0.2 2024-07-17 13:05:02 +00:00
Ottermandias
0320da0fc5 Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-07-17 02:36:31 +02:00
Ottermandias
124656c22e Fix advanced customize. 2024-07-17 02:36:01 +02:00
Actions User
1725dbb583 [CI] Updating repo.json for testing_1.3.0.1 2024-07-17 00:03:10 +00:00
Ottermandias
b3818a90df Fix design migrations and cma import. 2024-07-17 02:01:14 +02:00
Ottermandias
e7d5cf02e6 Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-07-17 00:31:40 +02:00
Ottermandias
3484a29f7a Disable shine parameter application rules. 2024-07-17 00:30:16 +02:00
Ottermandias
a807c90885 Disable the currently outdated preparecolorset hook. 2024-07-17 00:29:53 +02:00
Actions User
951c167058 [CI] Updating repo.json for testing_1.3.0.0 2024-07-16 21:45:00 +00:00
Ottermandias
25491adbe3 Rename Sclera to Limbal 2024-07-16 23:42:39 +02:00
Ottermandias
51c7fc83dd Woops. 2024-07-16 23:34:45 +02:00
Ottermandias
a5f35dbd17 Revert legacy tattoo changes. 2024-07-16 23:30:02 +02:00
Ottermandias
c75315f0f7 Update Repo. 2024-07-16 23:01:27 +02:00
Ottermandias
d3824d6a23 Fix some hrothgar gender change remainders. 2024-07-16 22:59:56 +02:00
Ottermandias
303001fed0 Update GameData. 2024-07-16 22:57:37 +02:00
Ottermandias
717f9eb393 Update GameData. 2024-07-16 22:10:35 +02:00
Ottermandias
9529963aa2 Update other things. 2024-07-16 18:19:34 +02:00
Ottermandias
81059411e5 Current state. 2024-07-15 15:19:51 +02:00
Ottermandias
7a602d6ec5 Extricate bonus slots somewhat. 2024-07-11 19:46:01 +02:00
Ottermandias
7caf6cc08a Initial Update for multiple stains, some facewear support, and API X 2024-07-11 14:21:25 +02:00
Ottermandias
c1d9af2dd0 Update Item API. 2024-07-11 14:20:19 +02:00
Ottermandias
3f99d11179 Add support info to Glamourer. 2024-06-23 22:55:31 +02:00
Ottermandias
52b89a4177 Do not apply auto designs to transformed actors maybe? 2024-06-15 11:53:04 +02:00
Ottermandias
5cf6b0eb75 Fix issue with Penumbra load order and visor state. 2024-06-15 11:53:04 +02:00
Actions User
205a95b254 [CI] Updating repo.json for 1.2.3.3 2024-06-11 10:50:56 +00:00
Ottermandias
cd498b539b Ugh. 2024-06-11 12:48:57 +02:00
Ottermandias
089c7d392d Update submodules. 2024-06-11 12:31:02 +02:00
Ottermandias
6207f3a67f Fix an exception. 2024-06-11 12:31:02 +02:00
Actions User
dec3598eff [CI] Updating repo.json for 1.2.3.2 2024-06-07 14:41:01 +00:00
Ottermandias
eb7cc6ffa0 Add IpcPending. 2024-06-05 11:29:04 +02:00
Actions User
960548f7b2 [CI] Updating repo.json for 1.2.3.1 2024-06-01 22:12:12 +00:00
Ottermandias
0f40906451 Update Penumbra.Api 2024-06-02 00:01:56 +02:00
Ottermandias
9a14f725b8 Add provider. 2024-06-01 20:39:58 +02:00
Ottermandias
0450c4e3f7 Add StateChangedWithType. 2024-06-01 20:26:18 +02:00
Ottermandias
86fc472144 Change Glamourer.Api to not use ssh. 2024-06-01 19:39:17 +02:00
Ottermandias
4c32ca6e63 use the last matching game object instead of the first for advanced dyes, specifically highlighting. 2024-06-01 19:39:17 +02:00
Ottermandias
67acdd2d13 Ignore unused values in gmp entries for serialization 2024-06-01 19:39:17 +02:00
Actions User
b7d482a24e [CI] Updating repo.json for 1.2.3.0 2024-06-01 09:55:40 +00:00
Ottermandias
8ce667e7e4 improve a code. 2024-05-31 23:03:04 +02:00
Ottermandias
9851533143 Update GameData 2024-05-31 23:03:04 +02:00
Ottermandias
87d51c04ad Add changelog. 2024-05-31 23:03:04 +02:00
Ottermandias
edd55087db Add hints to codes. 2024-05-31 23:03:04 +02:00
Actions User
9c49b66d71 [CI] Updating repo.json for testing_1.2.2.2 2024-05-30 12:13:52 +00:00
Ottermandias
448090e8f5 Better error message when not stupid. 2024-05-30 14:11:40 +02:00
Actions User
794cea72cc [CI] Updating repo.json for testing_1.2.2.1 2024-05-30 10:44:58 +00:00
Ottermandias
20983a5605 Fix reverting stains to game. 2024-05-29 15:45:07 +02:00
Ottermandias
284920b8fe Add check for Penumbra API mismatch. 2024-05-28 18:06:56 +02:00
Actions User
13181311ae [CI] Updating repo.json for testing_1.2.2.0 2024-05-27 15:45:54 +00:00
Ottermandias
1341c4316c Use Legacy Color Tables because our materials do not have DT ones yet. 2024-05-27 17:42:49 +02:00
Ottermandias
93dcd317c1 Update Submodules 2024-05-26 15:43:19 +02:00
Ottermandias
dbd11f6a95 Add initial changelog and preliminary data for existing cheatcodes, no hints yet. 2024-05-26 15:28:53 +02:00
Ottermandias
e4883b15cc Add a overwrite with player state button to design tab. 2024-05-23 23:40:09 +02:00
Ottermandias
91138176e0 Change API Version. 2024-05-20 18:13:41 +02:00
Ottermandias
6efd89e0ab Make this option work when applying automation. 2024-05-05 15:40:04 +02:00
Ottermandias
2713e6f1f6 Add an option for designs to always force a redraw. 2024-05-05 15:29:37 +02:00
Ottermandias
86c871fa81 Add initial customize chat command. 2024-05-02 00:59:24 +02:00
Ottermandias
8fff09f92e Some compatibility 2024-05-02 00:59:03 +02:00
Ottermandias
9d18707cb7 Update API. 2024-04-23 15:20:18 +02:00
Ottermandias
e8096f6e00 Add legacy IPC. 2024-04-23 15:13:09 +02:00
Ottermandias
e0447b1ed4 Fix height display. 2024-04-23 15:12:58 +02:00
Ottermandias
78d7aef13f Add height display in real-world units. 2024-04-19 01:05:15 +02:00
Ottermandias
552338e5b5 Improve IPC Providers. 2024-04-17 18:16:48 +02:00
Ottermandias
e50474f12d Fix Eye labels. 2024-04-17 18:16:33 +02:00
Ottermandias
dfb3ef3d79 Add some copy functionality for mod associations. 2024-04-15 23:30:45 +02:00
Ottermandias
4900ccb75a Make automatically applied offhands due to setting also apply mainhand dye. 2024-04-15 23:30:22 +02:00
Ottermandias
f949153292 Fix Apply Dye checkbox tooltip. 2024-04-15 23:29:54 +02:00
Ottermandias
efd51b0b5e Add Provider and tester for Base64. 2024-04-15 17:31:17 +02:00
Ottermandias
fb64315d51 Add documentation to API. 2024-04-15 16:54:02 +02:00
Ottermandias
ee78a2a983 Fix API Version. 2024-04-14 15:37:19 +02:00
Ottermandias
a722abb141 Add Api Submodule. 2024-04-14 15:35:06 +02:00
Ottermandias
21aa3e8efc Extract API to own project. 2024-04-14 15:30:39 +02:00
Ottermandias
0268546f63 Rework API/IPC and use new Penumbra IPC. 2024-04-14 15:07:31 +02:00
Ottermandias
9f276c7674 Update submodules. 2024-04-14 15:02:30 +02:00
Ottermandias
a1b40068e3 Update OtterGui. 2024-04-09 15:22:45 +02:00
Ottermandias
43d683ac66 Fix WeaponCombo missing favorite. 2024-04-09 15:21:19 +02:00
Ottermandias
9a52dddba3 Add design rename field to context. 2024-04-09 15:21:07 +02:00
Ottermandias
e35f2816e2 Make OpenConfigUi jump to settings. 2024-04-05 14:51:55 +02:00
Ottermandias
d81197a40f Add OpenMainUi. 2024-04-05 14:42:33 +02:00
Ottermandias
10e508b4e7 Fix visor service with umbrella on. 2024-04-05 14:42:23 +02:00
Ottermandias
7091fdd808 Fix API doc. 2024-04-05 13:28:54 +02:00
Ottermandias
f6e74c06cc Fix inefficient fetching of mod settings. 2024-04-05 13:27:40 +02:00
Ottermandias
12fa14e1c6 Fix some issues with self-named items. 2024-04-04 18:17:45 +02:00
Actions User
c573feefec [CI] Updating repo.json for testing_1.2.1.3 2024-04-04 12:04:19 +00:00
Ottermandias
0d427dcaba Fix some obsoletes. 2024-04-04 14:02:12 +02:00
Ottermandias
8375abd6cb Maybe fix visor state issue. 2024-04-04 14:01:58 +02:00
Ottermandias
3d421881f6 Maybe fix weapon behavior with multiple restricted designs with identical weapon types. 2024-03-28 15:23:19 +01:00
Ottermandias
71d6a658d6 Fix context menu. 2024-03-22 16:42:37 +01:00
Actions User
73122ad9bf [CI] Updating repo.json for testing_1.2.1.2 2024-03-22 12:38:05 +00:00
Ottermandias
9f3f78cf4c Remove DI reference. 2024-03-22 13:36:14 +01:00
Ottermandias
43c26f8499 Update OtterGui. 2024-03-22 13:32:55 +01:00
Ottermandias
02fc8452d2 Update ObjectManager 2024-03-21 23:53:57 +01:00
Actions User
eab1352340 [CI] Updating repo.json for testing_1.2.1.1 2024-03-20 21:27:47 +00:00
Ottermandias
633a01f176 Remove Debug Assert that triggers now apparently. 2024-03-20 22:25:50 +01:00
Actions User
411ab84d80 [CI] Updating repo.json for testing_1.2.1.0 2024-03-20 17:25:13 +00:00
Ottermandias
43d5700d72 Update actions. 2024-03-20 18:23:22 +01:00
Ottermandias
7c8bd514de Add Changelog. 2024-03-20 17:50:55 +01:00
Ottermandias
6269777760 Update GameData. 2024-03-20 17:36:13 +01:00
Ottermandias
2d75c24371 Use new context menu service. 2024-03-20 17:28:46 +01:00
Ottermandias
9750736d57 Use Penumbra.GameData Actor and Model and ObjectManager. 2024-03-19 23:19:46 +01:00
Ottermandias
05d261d4a3 Add Reapply Automation with prior functionality and rework header buttons for performance. 2024-03-15 15:36:31 +01:00
Actions User
8dde1689f7 [CI] Updating repo.json for 1.2.0.8 2024-03-14 15:24:31 +00:00
Ottermandias
6362c79aa2 Make penumbra preview work in gpose for all weapons. 2024-03-14 16:22:41 +01:00
Ottermandias
5ddf882077 Fix issue with materials. 2024-03-14 16:22:27 +01:00
Ottermandias
7af0a1d562 Fix revert to automation. 2024-03-14 16:22:14 +01:00
Ottermandias
fdd74c0514 Add some IPC stuff. 2024-03-09 12:25:59 +01:00
Actions User
78b15c085f [CI] Updating repo.json for 1.2.0.7 2024-03-08 15:55:11 +00:00
Ottermandias
2c0423d2b5 Fix color of special designs. 2024-03-08 16:45:30 +01:00
Ottermandias
e8907871af Fix random design ignoring last design. 2024-03-08 16:45:30 +01:00
Ottermandias
85d9dea2dd Fix some advanced state applications. 2024-03-08 16:45:30 +01:00
Actions User
c9160b8167 [CI] Updating repo.json for 1.2.0.6 2024-03-07 23:55:36 +00:00
Ottermandias
c99aa51f8a Fix sources, let invalid model state weapons reset to base if necessary, check design application against base type. 2024-03-08 00:49:21 +01:00
Actions User
ee426eb29f [CI] Updating repo.json for 1.2.0.5 2024-03-07 15:40:06 +00:00
Ottermandias
b5bdf52d16 Maybe fix weapon tracking? 2024-03-07 16:37:24 +01:00
Actions User
fdb9479f2d [CI] Updating repo.json for 1.2.0.4 2024-03-04 14:10:37 +00:00
Ottermandias
6696d539d3 Add quick selection design and saving last quick selected design in config. 2024-03-04 15:08:54 +01:00
Ottermandias
f35c20ed4d Maybe fix listening for moved items. 2024-03-04 15:08:54 +01:00
Actions User
ce3197cb25 [CI] Updating repo.json for 1.2.0.3 2024-03-03 12:46:42 +00:00
Ottermandias
9f9a58af2a Add an option to respect manual changes on changing automation. 2024-03-03 13:42:57 +01:00
Ottermandias
e6bd91319b Fix thrown exception by stupidity. 2024-03-03 00:53:02 +01:00
Actions User
62d89633bb [CI] Updating repo.json for 1.2.0.2 2024-03-02 15:13:24 +00:00
Ottermandias
93ba44d230 Change fixed state handling for advanced dyes. 2024-03-02 16:11:31 +01:00
Ottermandias
bade38f82e Handle other direction for bodytype. 2024-03-02 12:08:24 +01:00
Actions User
96fcf9e727 [CI] Updating repo.json for 1.2.0.1 2024-03-01 22:43:26 +00:00
Ottermandias
bfe50f459d Add option to apply designs to player with doubl click. 2024-03-01 23:41:30 +01:00
Ottermandias
64c1f75ee0 Store last selected design, apply that design and last selected tab. 2024-03-01 23:40:11 +01:00
Ottermandias
f07717240f Fix turning non-humans human with merged designs. 2024-03-01 23:39:09 +01:00
Ottermandias
4bbb48b7b9 Fix BodyType issues. 2024-03-01 23:38:28 +01:00
Ottermandias
a501d97252 Fix design link application checkboxes. 2024-03-01 16:41:51 +01:00
Actions User
22e7a71425 [CI] Updating repo.json for 1.2.0.0 2024-03-01 13:37:59 +00:00
Ottermandias
00c5c06629 Update Changelog. 2024-03-01 14:33:46 +01:00
Ottermandias
ed6f32f757 Improve QDB. 2024-03-01 14:29:36 +01:00
Ottermandias
436a975a44 Fix advanced dye buttons appearing in design panel. 2024-03-01 14:29:23 +01:00
Ottermandias
8496f86b6b Fix gear set changes and item move events not being instantiated. 2024-03-01 13:25:15 +01:00
Ottermandias
c5211ab779 Fix inverted logic. 2024-02-29 18:43:50 +01:00
Actions User
62aa9cabbe [CI] Updating repo.json for testing_1.1.0.18 2024-02-29 15:57:24 +00:00
Ottermandias
7f04cb7c45 Changelog. 2024-02-29 16:40:11 +01:00
Ottermandias
139508917b Add UI stuff to random designs. 2024-02-29 16:40:11 +01:00
Ottermandias
2a01b328e1 Fix redraw. 2024-02-29 16:40:11 +01:00
Ottermandias
1cf0e2f70e Add some new things, rework Revert-handling. 2024-02-29 16:40:11 +01:00
Actions User
f2951ca800 [CI] Updating repo.json for testing_1.1.0.16 2024-02-24 13:05:34 +00:00
Ottermandias
3e9edf3617 Make the player not update too often on changes. 2024-02-24 14:03:51 +01:00
Ottermandias
e4324bc4ce Update API. 2024-02-24 12:07:52 +01:00
Actions User
b8dc64eb1d [CI] Updating repo.json for testing_1.1.0.15 2024-02-23 15:52:08 +00:00
Ottermandias
ecf6008b71 Fix applying designs via weapons in gpose. 2024-02-23 16:49:13 +01:00
Actions User
fdee4c4ca8 [CI] Updating repo.json for testing_1.1.0.14 2024-02-22 21:53:32 +00:00
Ottermandias
80a6e89aa5 Fix problem with mare colors not resetting, reduce redraws again, use material texture instead of GPU. 2024-02-22 22:50:39 +01:00
Actions User
d36e4f891b [CI] Updating repo.json for testing_1.1.0.13 2024-02-22 17:34:29 +00:00
Ottermandias
53c2a7495f Make AutoRedraw for IPC wait a bit before applying. 2024-02-22 18:32:40 +01:00
Ottermandias
d8ce81cdc4 Run auto redraw on framework, add some locks, handle material value application differently for ApplyAll. 2024-02-22 18:10:45 +01:00
Ottermandias
e5f62d3ea9 Materials only when gear customization is on. 2024-02-22 18:06:26 +01:00
Ottermandias
5f6e24c34f Add chat command to apply items. 2024-02-20 17:42:09 +01:00
Actions User
62c1730a71 [CI] Updating repo.json for testing_1.1.0.12 2024-02-18 13:47:39 +00:00
Ottermandias
d6575e6e68 Handle state reapplication on temporary mod changes. 2024-02-18 14:45:54 +01:00
Ottermandias
cdaabc05e9 Fix bug with weapon colorsets, add table buttons. 2024-02-18 14:45:34 +01:00
Ottermandias
c85598acf4 Listen to temporary mod changes. 2024-02-18 13:06:19 +01:00
Ottermandias
22a8ba3f35 Make a reapply event. 2024-02-18 13:05:04 +01:00
Ottermandias
0bc9fc872e Add some valid options for material values. 2024-02-17 15:01:35 +01:00
Actions User
ef2d9ba207 [CI] Updating repo.json for testing_1.1.0.11 2024-02-17 12:54:55 +00:00
Ottermandias
e8f6b93610 Add changelog. 2024-02-17 13:49:18 +01:00
Ottermandias
a5509e8ba0 Finish advanced dye UI for now. 2024-02-17 13:49:08 +01:00
Ottermandias
59529476eb Further improvements. 2024-02-16 17:48:40 +01:00
Ottermandias
5f74f4b4d9 Add preview stuff. 2024-02-16 15:11:59 +01:00
Ottermandias
0f7d7caba8 Fix squared values. 2024-02-16 00:13:02 +01:00
Ottermandias
a2843c1298
Merge pull request #95 from Caraxi/update-associated-design
Add ability to update configuration of associated mod
2024-02-15 16:32:54 +01:00
Caraxi
fb7a92b72a Add ability to update configuration of associated mod 2024-02-15 19:05:27 +10:30
Ottermandias
10962cac6c Improve advanced dye stuff. 2024-02-14 18:51:57 +01:00
Ottermandias
a194f88903 Add overrides to associated collections. 2024-02-14 18:51:57 +01:00
Actions User
02dff90dd0 [CI] Updating repo.json for testing_1.1.0.10 2024-02-13 12:42:38 +00:00
Ottermandias
3537406eaa Fix IPC Labels. 2024-02-13 00:52:30 +01:00
Ottermandias
f5666680ff Make customizations right-clickable in penumbra. 2024-02-13 00:24:40 +01:00
Actions User
72f1663e28 [CI] Updating repo.json for testing_1.1.0.9 2024-02-12 19:02:13 +00:00
Ottermandias
c40853e5b2 Fix customize check to use correct gender/clan. 2024-02-12 19:59:50 +01:00
Ottermandias
488bea0e78 Make ManualWithLinks the default. 2024-02-12 19:59:50 +01:00
Ottermandias
b4cd5110f2 Set Application rules to All for GetCustomization. 2024-02-12 19:59:50 +01:00
Ottermandias
7dfc20ce23 tmp 2024-02-12 19:59:50 +01:00
Actions User
7a33daf954 [CI] Updating repo.json for testing_1.1.0.8 2024-02-07 01:58:03 +00:00
Ottermandias
ec0f7a2d1e Fix revert design base state. 2024-02-07 02:56:12 +01:00
Actions User
088066f68a [CI] Updating repo.json for testing_1.1.0.7 2024-02-07 00:41:38 +00:00
Ottermandias
b228658414 Fix weapon state updating logic. 2024-02-07 01:39:36 +01:00
Ottermandias
7653fd22c0 Fix meta state updating logic. 2024-02-07 01:39:36 +01:00
Ottermandias
0ea6e6dac5 Fix crest application rules not copying on duplicates. 2024-02-07 01:39:36 +01:00
Actions User
e967c17d84 [CI] Updating repo.json for testing_1.1.0.6 2024-02-06 18:51:39 +00:00
Ottermandias
836e3e6603 Fix dumb bug. 2024-02-06 19:49:17 +01:00
Actions User
28ce293d49 [CI] Updating repo.json for testing_1.1.0.5 2024-02-06 17:07:21 +00:00
Ottermandias
dd5463071e Add changelog. 2024-02-06 18:05:07 +01:00
Ottermandias
fc52d44c9c Improve some mousewheel stuff and add some tooltips. 2024-02-06 18:01:05 +01:00
Ottermandias
346e4890b0 Improve link UI a bit. 2024-02-06 17:37:26 +01:00
Ottermandias
73266811a0 Merge branch 'colortable' 2024-02-06 16:51:44 +01:00
Ottermandias
99181d2fdb Disable Material stuff for now. 2024-02-06 16:51:12 +01:00
Ottermandias
b5b9289dc2 Change mousewheel to ctrl, current material state. 2024-02-06 16:42:43 +01:00
Ottermandias
42ac507b86 Improve festivals and add dolphins. 2024-02-05 16:21:21 +01:00
Ottermandias
1fefe7366c MouseWheels are pretty great. 2024-02-05 15:21:29 +01:00
Ottermandias
5e37f8d2e8 add some glamour debug stuff. 2024-02-02 23:27:56 +01:00
Ottermandias
fb7aac5228 Add state editing and tracking. 2024-02-01 13:52:20 +01:00
Ottermandias
5cdcb9288e Add NearEqual for vectors. 2024-02-01 13:51:38 +01:00
Ottermandias
d10043a69a Add toggle for always applying mod associations. 2024-01-30 18:30:51 +01:00
Ottermandias
818bf71032 Add IpcManual state. 2024-01-30 17:51:52 +01:00
Ottermandias
994b7bfb6c Add clone function to MaterialValueManager. 2024-01-30 16:09:58 +01:00
Ottermandias
eea4de63d5 Fix inverted logic. 2024-01-30 16:09:30 +01:00
Ottermandias
502b2439b4 Fix application rule display for meta. 2024-01-30 16:05:07 +01:00
Ottermandias
962c4e53ad Better handling of application rules. 2024-01-30 16:04:56 +01:00
Ottermandias
cb45221be2 Things are progressing at a satisfying rate. 2024-01-27 00:32:48 +01:00
Ottermandias
5e5ce4d234 Add functions to compute current color table. 2024-01-26 17:16:57 +01:00
Ottermandias
beff7adec4 Add Vortice. 2024-01-26 16:51:38 +01:00
Ottermandias
447e748ed7 start 2024-01-26 16:51:38 +01:00
Ottermandias
0e3d3d1839 Fix application rules disrespect for auto designs. 2024-01-26 16:51:07 +01:00
Ottermandias
eba27e10fb Merge branch 'designlinks' 2024-01-26 16:45:51 +01:00
Ottermandias
5992b86e4f Add working links. 2024-01-26 16:45:31 +01:00
Ottermandias
282d6df165 Add UI. 2024-01-26 16:10:09 +01:00
Ottermandias
2219d9293f Use new functionality. 2024-01-26 15:04:36 +01:00
Ottermandias
a4de13f228 Make states work. 2024-01-26 14:33:27 +01:00
Ottermandias
25ddbb1310 Fix issue with buttons sharing state. 2024-01-26 00:18:08 +01:00
Ottermandias
b92dc03eb5 Use IDesignEditor for designs. 2024-01-25 16:51:37 +01:00
Ottermandias
46fcac6c7d Misc. 2024-01-25 16:14:27 +01:00
Ottermandias
fce8b058b0 Improve DesignStorage slightly. 2024-01-25 15:56:49 +01:00
Ottermandias
a284d5adc5 Improve StateIndex. 2024-01-24 15:42:35 +01:00
Ottermandias
b6549899e8 Add option to apply entire weapon. 2024-01-24 15:42:24 +01:00
Ottermandias
5fd4a83aa4 Improve StateIndex. 2024-01-23 18:51:58 +01:00
Ottermandias
1ad70541d3 Reworked all of the meta, made StateIndex its own thing. 2024-01-23 18:02:53 +01:00
Ottermandias
70e4833fb5 Fix customize parameters for NPC target application. 2024-01-22 23:50:19 +01:00
Ottermandias
4f9e224494 Move MetaIndex to its first class enum. 2024-01-22 23:49:56 +01:00
Ottermandias
e4fc86ca38 Protect against an exception but actual fix is in Penumbra. 2024-01-22 22:46:51 +01:00
Ottermandias
96c4ae762e Make AutoDesignApplier use MergedDesign. 2024-01-21 00:51:42 +01:00
Ottermandias
2422295e67 Support states better in merged designs. 2024-01-21 00:48:53 +01:00
Ottermandias
5ec112d896 Primary constructor. 2024-01-21 00:47:37 +01:00
Ottermandias
18683f7d43 Update the elephants. 2024-01-20 17:01:57 +01:00
Ottermandias
c7430e59b3 Start 2024-01-20 15:13:23 +01:00
Ottermandias
1a409d475a Increase version. 2024-01-19 13:24:24 +01:00
Ottermandias
59131ec191 Fix load order dependency on weapon load. 2024-01-18 22:44:55 +01:00
Ottermandias
092e0ee30e Make it not use clipboard because duh. 2024-01-18 00:59:53 +01:00
Ottermandias
593bb47241 Reorder names. 2024-01-18 00:55:47 +01:00
Ottermandias
d13e3ccbd7 Add some better handling for Highlights, add copy/paste buttons for colors. 2024-01-18 00:50:06 +01:00
Ottermandias
805e192d63 Add alpha preview. 2024-01-17 17:47:30 +01:00
Actions User
4c9f362f21 [CI] Updating repo.json for 1.1.0.4 2024-01-17 16:29:43 +00:00
Ottermandias
cd50950dae 1.1.0.4 2024-01-17 17:27:48 +01:00
Ottermandias
53c4dfeee5 Add color display options. 2024-01-17 17:22:32 +01:00
Ottermandias
4abae59974 Fix another color thing. 2024-01-17 16:27:51 +01:00
Ottermandias
42aa4fb4a2 Add a button to revert advanced customizations only to game state. 2024-01-17 15:21:29 +01:00
Ottermandias
8b8f85dd85 Fixed handling of Pending in automation. 2024-01-17 14:29:30 +01:00
Ottermandias
3b5f89e6a1 Fix palettes not resetting on Reapply Automation with Use Game State as Base. 2024-01-17 13:31:22 +01:00
Ottermandias
1ab9d5a2a7 Fix bodytype not transmitting through Mare? 2024-01-17 13:01:58 +01:00
Ottermandias
51a9de3108 Update BNPCs. 2024-01-17 12:15:34 +01:00
Ottermandias
ee0bdace30 Fix application rule labels. 2024-01-17 11:59:00 +01:00
Actions User
0cb2933a0e [CI] Updating repo.json for 1.1.0.3 2024-01-17 01:12:36 +00:00
Ottermandias
5a30107d78 Add check for Palette+. 2024-01-17 01:35:27 +01:00
Ottermandias
78a77eb6c4 Fix fistweapon hack. 2024-01-17 00:51:39 +01:00
Ottermandias
547c40fe52 Fix advanced parameters not applying after race change. 2024-01-17 00:38:47 +01:00
Ottermandias
57f4ccaaa5 Update restricted items. 2024-01-17 00:38:47 +01:00
Actions User
0430d994f7 [CI] Updating repo.json for 1.1.0.2 2024-01-16 21:16:45 +00:00
Ottermandias
68655cf309 1.1.0.2 2024-01-16 22:15:00 +01:00
Ottermandias
d2fd8f839b Fix some stuff. 2024-01-16 22:11:54 +01:00
Ottermandias
78ec0f950f Fix gamedata. 2024-01-16 22:11:54 +01:00
Ottermandias
831908475c Add tooltip to percentage selector and improve value drag range. 2024-01-16 22:11:54 +01:00
Ottermandias
ed0ee6439a Improve Palette+ Import. 2024-01-16 22:11:54 +01:00
Ottermandias
13bf83b2b7 Add design color to preview in combos. 2024-01-16 22:11:54 +01:00
Ottermandias
c245b30eaa Fix clone not copying parameter rules. 2024-01-16 22:11:53 +01:00
Ottermandias
d57d40bd59 Fix name of customize parameters. 2024-01-16 22:11:53 +01:00
Actions User
8d4f71122c [CI] Updating repo.json for 1.1.0.1 2024-01-16 17:24:06 +00:00
Ottermandias
cfa35b2379 Update Gamedata. 2024-01-16 18:19:49 +01:00
Ottermandias
34fa1e37c8 Fix issue with null-favorites. 2024-01-16 17:54:36 +01:00
Actions User
6a3fb7f599 [CI] Updating repo.json for 1.1.0.0 2024-01-16 15:40:07 +00:00
Ottermandias
917e80d467 Prepare changelog. 2024-01-16 16:15:58 +01:00
Ottermandias
c87885bd3b Store Model ID for NPCs, fix issue with applying NPC data setting extended colors. 2024-01-12 23:35:41 +01:00
Ottermandias
a50b63f67e Add Palette+ Import. 2024-01-12 13:46:45 +01:00
Ottermandias
ada0c6f479 Add customization options to favorites, currently only hairstyles and facepaints. 2024-01-12 00:02:46 +01:00
Actions User
47e222a9a9 [CI] Updating repo.json for testing_1.0.7.8 2024-01-11 18:22:22 +00:00
Ottermandias
d45e47378b Fix dumb. 2024-01-11 19:19:42 +01:00
Actions User
bc69e6d0ad [CI] Updating repo.json for testing_1.0.7.7 2024-01-11 11:42:48 +00:00
Ottermandias
50b1b64141 Add lip opacity migration for Nova. 2024-01-11 12:41:03 +01:00
Ottermandias
8a9fa98706 Make LipDiffuse a Vec4 instead of two values. 2024-01-10 16:41:45 +01:00
Ottermandias
630647b544 Fix some issues with parameters. 2024-01-10 15:44:19 +01:00
Ottermandias
ed27b1dff4 Use advanced parameters from IPC regardless of setting. 2024-01-09 18:30:01 +01:00
Actions User
5d0993a9ce [CI] Updating repo.json for testing_1.0.7.6 2024-01-09 16:34:53 +00:00
Ottermandias
c06f617e04 Some more changes. 2024-01-09 17:33:01 +01:00
Actions User
7b6e037e5f [CI] Updating repo.json for testing_1.0.7.5 2024-01-09 16:02:32 +00:00
Ottermandias
a2731b4010 Fix after-gpose bug. 2024-01-09 16:54:54 +01:00
Ottermandias
bb671e8dd2 Add revert options to equip. 2024-01-09 16:54:43 +01:00
Ottermandias
5ea779a34c Add decal color, fix some bugs and improve logic and handling somewhat. 2024-01-09 16:05:26 +01:00
Actions User
6158bcb2f9 [CI] Updating repo.json for testing_1.0.7.4 2024-01-08 22:40:43 +00:00
Ottermandias
31bff511b8 Update OtterGui. 2024-01-08 23:36:57 +01:00
Ottermandias
a5c33a6311 Use global usings for System headers. 2024-01-08 23:30:55 +01:00
Ottermandias
d62d7e352f Add option to apply mod associations with /glamour apply. 2024-01-08 23:25:51 +01:00
Ottermandias
1a0a0f681f Add parameter handling. 2024-01-08 23:00:02 +01:00
Ottermandias
9361560350 Add CustomizeParameter data. 2024-01-08 22:58:44 +01:00
Ottermandias
bbf460f5e0 re-fix slot check in restricted gear. 2024-01-07 22:21:55 +01:00
Ottermandias
6ecf06a671 Update OtterGui 2024-01-06 23:56:19 +01:00
Ottermandias
6130bae81d I don't know what behavior changed here but this fixes the issue. 2024-01-03 22:47:44 +01:00
Actions User
c3f2e7d3a1 [CI] Updating repo.json for testing_1.0.7.3 2023-12-31 12:46:43 +00:00
Ottermandias
5f28644b56 Merge branch 'main' into Limiana/main 2023-12-31 13:44:24 +01:00
Ottermandias
a5c1e66916 Rename and move and reuse. 2023-12-31 13:29:42 +01:00
Ottermandias
fca5e83841 Fix IPC Tester after adding Stain. 2023-12-31 13:16:59 +01:00
Ottermandias
2e5cdc229d Improve PR to use GetDesign. 2023-12-31 13:16:47 +01:00
Ottermandias
2642f9e7bc Merge branch 'main' of github.com:Ottermandias/Glamourer 2023-12-31 12:59:52 +01:00
Ottermandias
be33823f9d
Merge pull request #93 from X3llus/delete-function
Design Deletion Command
2023-12-31 12:59:46 +01:00
Ottermandias
ea7806535a Fix left rings not being changeable in designs. 2023-12-31 12:59:28 +01:00
Ottermandias
060e8047ca Add Stain to IPC Set. 2023-12-31 12:59:15 +01:00
X3llus
0a3ca24303 removed commeted out code 2023-12-30 11:06:46 -05:00
X3llus
9b82f856e1 small formatting fix 2023-12-30 11:04:47 -05:00
X3llus
c29b6b5e57 Created a new glamour command that lets you delete a desgin using the given designs name 2023-12-30 10:52:59 -05:00
Limiana
9395072bee Adjust new IPC methods + added testing methods 2023-12-29 21:33:21 +03:00
Ottermandias
22ea1e344e do not protect when state is locked. 2023-12-29 19:01:13 +01:00
Actions User
6c5c202356 [CI] Updating repo.json for testing_1.0.7.2 2023-12-29 17:42:37 +00:00
Ottermandias
24c3a52f6a Fix design coloring and Apply All Customization display. 2023-12-29 18:38:50 +01:00
Ottermandias
53388739ca Fix labels appearing where unwanted. 2023-12-29 18:38:50 +01:00
Actions User
ff6905d45e [CI] Updating repo.json for testing_1.0.7.1 2023-12-29 16:32:15 +00:00
Ottermandias
5faaf5337e Fix time display. 2023-12-29 17:28:16 +01:00
Ottermandias
f4dfe8e89c Add Crown Code. 2023-12-29 00:12:14 +01:00
Ottermandias
29799094ea Adjust world weights. 2023-12-28 19:34:24 +01:00
Ottermandias
0c1dd50890 Add NPC appearance tab. 2023-12-28 19:01:28 +01:00
Ottermandias
4b242bb3cf Change design loading. 2023-12-28 14:20:59 +01:00
Ottermandias
dd5c56de9d Rework codes and fun a bit. 2023-12-26 17:37:35 +01:00
Limiana
6fe68c59d1
Merge branch 'Ottermandias:main' into main 2023-12-25 16:02:07 +03:00
Limiana
deba61d721 Added certain IPC methods 2023-12-25 16:01:49 +03:00
Ottermandias
1fa9afb9c6 Improve bodytype stuff. 2023-12-24 16:17:44 +01:00
Ottermandias
a900219ede Make crests more quiet. 2023-12-24 16:16:37 +01:00
Ottermandias
fcca756e20 Allow change of bodytype. 2023-12-24 14:37:36 +01:00
Ottermandias
d81a6b7f6f Set Order for customize again. 2023-12-24 14:37:04 +01:00
Ottermandias
44a65f61fb Add diagnostic display and make CustomizeManager a DataContainer. 2023-12-24 13:38:29 +01:00
Ottermandias
4531cdadbe Move more DebugUi to GameData. 2023-12-24 12:36:11 +01:00
Ottermandias
ab76d3508b Rework and improve CustomizationManager and stuff. 2023-12-23 19:33:50 +01:00
Ottermandias
aae4141550 Add IPC to set single items. 2023-12-23 13:08:20 +01:00
Ottermandias
03a0cb5514 Update Gamedata. 2023-12-22 14:22:39 +01:00
Ottermandias
987c26a51d Remove GameData, move a bunch of customization data to Penumbra.GameData and the rest to Glamourer, update accordingly. Some reformatting and cleanup. 2023-12-22 14:20:50 +01:00
Ottermandias
e9d0e61b4c Merge branch 'main' into dev 2023-12-21 23:27:33 +01:00
Ottermandias
36970e3275 Fix weapon changing in designs not working right. 2023-12-21 23:27:11 +01:00
Ottermandias
648b3d4515 Huh. 2023-12-21 17:05:24 +01:00
Ottermandias
3071599d94 Merge tag '1.0.7.0' into dev 2023-12-21 16:49:23 +01:00
Actions User
181f58f75f [CI] Updating repo.json for 1.0.7.0 2023-12-21 14:31:33 +00:00
Ottermandias
f2ea528316 1.0.7.0 2023-12-21 15:29:36 +01:00
Ottermandias
b0a89b7c19 Again. 2023-12-21 15:22:31 +01:00
Ottermandias
a982c0a1c1 Update gamedata and services. 2023-12-21 15:06:56 +01:00
Ottermandias
36d95c37bc Make compile job not depend on git. 2023-12-20 16:47:37 +01:00
Ottermandias
5b648ea2a0 Do not crash when failing to load textures. 2023-12-20 16:46:33 +01:00
Ottermandias
e37f16eb15 Remove unused state. 2023-12-20 16:46:20 +01:00
Ottermandias
768354be31 Update one set. 2023-12-19 16:42:21 +01:00
Ottermandias
4b92eae723 Rework debug tab. 2023-12-13 16:46:23 +01:00
Ottermandias
a04b7cd1db Add function to obtain items and stains from appearance data. 2023-12-13 16:45:33 +01:00
Ottermandias
a7b1d45b75 Make filter combo tooltips always enabled. 2023-12-04 16:25:56 +01:00
Actions User
6ee1501c09 [CI] Updating repo.json for testing_1.0.6.3 2023-12-02 20:53:01 +00:00
Ottermandias
e317b3683f Fix issues with meta toggles. 2023-12-02 21:47:35 +01:00
Actions User
dd289e6bd7 [CI] Updating repo.json for testing_1.0.6.2 2023-12-02 17:04:01 +00:00
Ottermandias
2cafa4e32b Update workflows and submodules. 2023-12-02 18:02:00 +01:00
Ottermandias
69dce5790b Merge branch 'Crests' 2023-12-02 17:58:07 +01:00
Ottermandias
11ab85545f Some Stuff 2023-12-02 17:56:23 +01:00
Actions User
f414eedf7b [CI] Updating repo.json for 1.0.6.1 2023-12-02 16:56:17 +00:00
Ottermandias
cc09cced61 Revamp, temp state. 2023-12-02 17:55:17 +01:00
Ottermandias
358e33346f Rework some stuff, add debug tab. 2023-12-02 17:55:17 +01:00
Ottermandias
668d4c033f Add CrestService. 2023-12-02 17:55:17 +01:00
Ottermandias
3177e6ca29 Update OtterGui. 2023-12-02 17:55:17 +01:00
Ottermandias
cd0196ddb4 UI for crests. 2023-12-02 17:55:17 +01:00
Ottermandias
512d0a1a5f Add interop for Actor and Model. 2023-12-02 17:55:17 +01:00
Ottermandias
6f4a7661d7 Add crest changing in designs. 2023-12-02 17:55:17 +01:00
Ottermandias
2f1b85a02a Add Crest flags. 2023-12-02 17:55:17 +01:00
Ottermandias
87a645b2a3 Try using mainhand item for vfx weapons in some cases. 2023-12-02 17:55:17 +01:00
Ottermandias
cdefe64e4e Allow import of .cma files. 2023-12-02 17:55:17 +01:00
Ottermandias
60a53d4bff Refactor drawing of equipment to be more sane. 2023-12-02 17:55:17 +01:00
Ottermandias
eed11bb67f Fix HQ Item IDs in inventory service. 2023-12-02 17:55:17 +01:00
Ottermandias
8412c2bc68 Add NoDocking flags to QDB. 2023-12-02 17:55:17 +01:00
Ottermandias
c79997dba8 Use https submodule. 2023-12-02 17:55:17 +01:00
Ottermandias
dd42b7ab7f Add experimental automation condition for gearsets. 2023-12-02 17:55:17 +01:00
Ottermandias
c11bd629da Save off the main thread. 2023-12-02 17:55:17 +01:00
Ottermandias
cd289247e9 Update OtterGui. 2023-12-02 17:55:17 +01:00
Ottermandias
294cbd4653 Update OtterGui. 2023-12-02 17:55:17 +01:00
Ottermandias
cf566932f9 Fix some issues with NPC Equip. 2023-12-02 17:55:17 +01:00
Ottermandias
e1fc08fce7 Allow parsing strings without backwards compatibility. 2023-12-02 17:53:43 +01:00
Actions User
0a7d800706 [CI] Updating repo.json for 1.0.6.0 2023-11-22 14:21:54 +00:00
Ottermandias
a373537adf 1.0.6.0 2023-11-22 15:19:54 +01:00
Ottermandias
2c3f7fb92a Brio compatibility. 2023-11-22 14:40:49 +01:00
Ottermandias
380e682cb4 Merge branch 'main' of github.com:Ottermandias/Glamourer 2023-11-21 17:40:19 +01:00
Ottermandias
321c481c7d Add option to import .chara files as design or onto actors/designs. 2023-11-21 17:40:06 +01:00
Actions User
9c3c1bdf59 [CI] Updating repo.json for testing_1.0.5.3 2023-11-18 12:18:47 +00:00
Ottermandias
226dbdd4a8 Improve multi design selection. 2023-11-18 13:16:51 +01:00
Ottermandias
0583cc5bfc Allow filtering for None in certain cases. 2023-11-18 13:16:33 +01:00
Ottermandias
b4b104f919 Add Ephemeral Config. 2023-11-17 17:09:38 +01:00
Actions User
9c8e9f5ead [CI] Updating repo.json for testing_1.0.5.2 2023-11-16 19:58:18 +00:00
Ottermandias
f514f79fe9 Fix dumb. 2023-11-16 20:55:48 +01:00
Actions User
98c793eafc [CI] Updating repo.json for testing_1.0.5.1 2023-11-16 18:56:14 +00:00
Ottermandias
7b939c81d0 Allow filtering for associated color. 2023-11-16 19:54:26 +01:00
Ottermandias
2b30a88bf4 Add support for custom colors for design display. 2023-11-16 19:43:04 +01:00
Ottermandias
ec7a53bee2 Improve (hopefully) some handling of options and stuff. Remove localization because wonky. 2023-11-16 18:21:50 +01:00
Ottermandias
68327d3563 Do a bunch of refactoring regarding available application options. 2023-11-16 17:32:55 +01:00
Ottermandias
f55d25b088 Improve tri-state checkboxes. 2023-11-15 18:38:42 +01:00
Ottermandias
2afa5734f7 Fix display of customization names in application rules. 2023-11-15 18:37:54 +01:00
Ottermandias
053998e5e4 Fix text color for customizations on locked designs. 2023-11-15 18:37:03 +01:00
Ottermandias
c4bb24a6ec No border around quick design window. 2023-11-12 13:46:45 +01:00
Ottermandias
8f60688e44 Fix design color display on customizations. 2023-11-12 13:46:45 +01:00
Ottermandias
108cfbd828 Add option to open window at game start instead of coupling it with debug mode. 2023-11-12 13:46:45 +01:00
Actions User
1891957670 [CI] Updating repo.json for 1.0.5.0 2023-11-11 13:18:43 +00:00
Ottermandias
09b0db977f 1.0.5.0 2023-11-11 14:16:21 +01:00
Ottermandias
879b8e49a0 Update BNPC Data. 2023-11-11 14:00:29 +01:00
Ottermandias
8ba6a9fa33 Open main window when selecting tab. 2023-11-11 13:56:38 +01:00
Ottermandias
823a8606d3 Add /glamour help text. 2023-11-10 23:12:26 +01:00
Ottermandias
859c738080 Allow undoing overwriting designs. 2023-11-10 19:00:33 +01:00
Ottermandias
36f6c48f7a Allow filtering designs for contained items. 2023-11-10 18:30:01 +01:00
Ottermandias
a3583dd5f1 Remove Enabled Config. 2023-11-10 18:21:26 +01:00
Ottermandias
eb6e665147 Fix exception throwing on no saved designs. Allow mousewheel scrolling of the combo. 2023-11-10 18:21:13 +01:00
Ottermandias
4328f5d680 Make colors favoritable. 2023-11-10 17:30:27 +01:00
Ottermandias
6ad9b56239 Fix lack of redraw after GPose. 2023-11-03 15:24:33 +01:00
Actions User
3d23923c52 [CI] Updating repo.json for 1.0.4.3 2023-10-31 11:34:20 +00:00
Ottermandias
900953c249 Give Witches panties. 2023-10-31 12:32:04 +01:00
Actions User
9f970a1920 [CI] Updating repo.json for 1.0.4.2 2023-10-31 11:30:09 +00:00
Ottermandias
d1b6ec2159 Refix fix. 2023-10-31 12:27:42 +01:00
Actions User
5e22946666 [CI] Updating repo.json for 1.0.4.1 2023-10-31 11:24:22 +00:00
Ottermandias
53b9aa9387 Make popups not appear when the player is busy. 2023-10-31 11:30:30 +01:00
Ottermandias
b7cd6dfe2d Fix log spam on nonexistent models. 2023-10-31 11:30:30 +01:00
Actions User
66cb6ffaad [CI] Updating repo.json for 1.0.4.0 2023-10-30 14:15:28 +00:00
Ottermandias
7716e4cc2a 1.0.4.0 2023-10-30 15:11:18 +01:00
Ottermandias
d597793772 Use local time when resetting festivals. 2023-10-30 15:05:18 +01:00
Ottermandias
81395f761c Remove Thaumaturges moccasins from restricted gear. 2023-10-30 15:05:01 +01:00
Ottermandias
bdcc6cb4de Handle ItemOffhand and fix weapon plugin load order dependency. 2023-10-28 01:14:38 +02:00
Actions User
4618e73c25 [CI] Updating repo.json for testing_1.0.3.2 2023-10-23 12:49:34 +00:00
Ottermandias
c0f19a24e4 Update API. 2023-10-23 14:47:02 +02:00
Ottermandias
30c5cd0bdb Try to handle transformations a bit better, maybe. 2023-10-23 14:43:11 +02:00
Ottermandias
8b8b14f2b6 Show and update BNPCs. 2023-10-17 19:51:17 +02:00
Ottermandias
0e943b5d1d Allow GPose Target to work as Target. 2023-10-17 19:49:49 +02:00
Actions User
27f1fcd422 [CI] Updating repo.json for 1.0.3.1 2023-10-16 13:09:46 +00:00
Ottermandias
7a94c4ee07 Improve unlocks table. 2023-10-16 15:05:17 +02:00
Ottermandias
909966d411 Improve color handling. 2023-10-16 15:04:58 +02:00
Ottermandias
27c41cac49 Add locking and color options and commands. 2023-10-12 16:56:18 +02:00
Ottermandias
a84a66a344 Set no default key combination for Quick Bar 2023-10-12 16:56:18 +02:00
Actions User
e2ddde81d9 [CI] Updating repo.json for 1.0.3.0 2023-10-11 23:08:39 +00:00
Ottermandias
1b0c432680 1.0.3.0 2023-10-12 01:06:32 +02:00
Ottermandias
ba81d585d4 Update ChangeCustomize to call the original by address for Palette+ compatibility. 2023-10-11 22:43:48 +02:00
Ottermandias
6dbac4e084 Disable sounds for changelog windows. 2023-10-11 17:24:26 +02:00
Ottermandias
277b26cc92 Add a quick design bar. 2023-10-11 17:22:53 +02:00
Ottermandias
56303be6ae
Merge pull request #89 from Haselnussbomber/disablewindowsounds
Disable window sounds in GenericPopupWindow
2023-10-10 22:01:09 +02:00
Haselnussbomber
09493984c0
Disable window sounds in GenericPopupWindow 2023-10-10 21:41:12 +02:00
Actions User
4ad2c84fce [CI] Updating repo.json for 1.0.2.2 2023-10-09 22:18:51 +00:00
278 changed files with 29546 additions and 11812 deletions

View file

@ -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/*

View file

@ -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
View file

@ -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

@ -0,0 +1 @@
Subproject commit 5b6730d46f17bdd02a441e23e2141576cf7acf53

View file

@ -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),
};
}
}
}

View file

@ -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>();
}
}
}

View file

@ -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,
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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>();
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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";
}

View file

@ -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,
};
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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
View 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
View 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;
}

View 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;
}

View file

@ -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);
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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
View 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
View 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);
}

View 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;
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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,

View file

@ -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);

View file

@ -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>

View 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);
}
}
}

View 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);
}

View 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;
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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)

View 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;
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}

View 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;
}
}

View file

@ -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)

View file

@ -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>

View 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;
}

View 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;
}
}
}

View 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);
}
}

View 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);
}

View 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);
}

View 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; }
}

View 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,
};

View 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));
}

View 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);
}
}
}

View 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);
}
}
}

View 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,
};
}
}

View 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;
}

View 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,
};
}

View 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;
}

View 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;
}

View 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));
}

View 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)}}}";
}
}

View 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;
}

View 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);
}
}

View 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,
}
}

View file

@ -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);
}

View 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,
}
}

View file

@ -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);
}

View 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,
}
}

View 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,
}
}

View file

@ -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
{

View 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,
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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);
}

View 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,
}
}

View file

@ -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);
}

View 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,
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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];
}

View file

@ -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);
}

View 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; }
}

View 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 = &parameter)
{
return ((float*)ptr)[23];
}
}
private static unsafe ref float GetUvOffsetWrite(ref CustomizeParameter parameter)
{
// TODO CS Update
fixed (CustomizeParameter* ptr = &parameter)
{
return ref ((float*)ptr)[23];
}
}
private static unsafe float GetUvMultiplier(in CustomizeParameter parameter)
{
// TODO CS Update
fixed (CustomizeParameter* ptr = &parameter)
{
return ((float*)ptr)[15];
}
}
private static unsafe ref float GetUvMultiplierWrite(ref CustomizeParameter parameter)
{
// TODO CS Update
fixed (CustomizeParameter* ptr = &parameter)
{
return ref ((float*)ptr)[15];
}
}
}

View 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,
};
}

View 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