Compare commits

...

520 commits

Author SHA1 Message Date
Actions User
360c8bb92a [CI] Updating repo.json for 1.5.1.8 2026-01-28 23:06:33 +00:00
Ottermandias
e96134a134 Fix offhand changes with empty offhand. 2026-01-28 17:58:14 +01:00
Ottermandias
74517d8ec5 Add ModUsage subscription. 2026-01-24 15:23:20 +01:00
Ottermandias
4d466fb7eb Fix minor issues. 2026-01-24 15:22:56 +01:00
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
241 changed files with 14029 additions and 6338 deletions

View file

@ -9,13 +9,15 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v5
with: with:
submodules: recursive submodules: recursive
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '7.x.x' dotnet-version: |
10.x.x
9.x.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Download Dalamud - name: Download Dalamud
@ -37,7 +39,7 @@ jobs:
- name: Archive - name: Archive
run: Compress-Archive -Path Glamourer/bin/Release/* -DestinationPath Glamourer.zip run: Compress-Archive -Path Glamourer/bin/Release/* -DestinationPath Glamourer.zip
- name: Upload a Build Artifact - name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.1 uses: actions/upload-artifact@v4
with: with:
path: | path: |
./Glamourer/bin/Release/* ./Glamourer/bin/Release/*

View file

@ -9,13 +9,15 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v5
with: with:
submodules: recursive submodules: recursive
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '7.x.x' dotnet-version: |
10.x.x
9.x.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Download Dalamud - name: Download Dalamud
@ -37,7 +39,7 @@ jobs:
- name: Archive - name: Archive
run: Compress-Archive -Path Glamourer/bin/Debug/* -DestinationPath Glamourer.zip run: Compress-Archive -Path Glamourer/bin/Debug/* -DestinationPath Glamourer.zip
- name: Upload a Build Artifact - name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.1 uses: actions/upload-artifact@v4
with: with:
path: | path: |
./Glamourer/bin/Debug/* ./Glamourer/bin/Debug/*

4
.gitmodules vendored
View file

@ -14,3 +14,7 @@
path = Penumbra.Api path = Penumbra.Api
url = https://github.com/Ottermandias/Penumbra.Api.git url = https://github.com/Ottermandias/Penumbra.Api.git
branch = main 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

@ -6,7 +6,10 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
.github\workflows\release.yml = .github\workflows\release.yml
Glamourer\Glamourer.json = Glamourer\Glamourer.json
repo.json = repo.json repo.json = repo.json
.github\workflows\test_release.yml = .github\workflows\test_release.yml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}"
@ -19,32 +22,38 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{EF233CE2-F243-449E-BE05-72B9D110E419}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{EF233CE2-F243-449E-BE05-72B9D110E419}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.Api", "Glamourer.Api\Glamourer.Api.csproj", "{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|Any CPU {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|Any CPU {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|Any CPU {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|Any CPU {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|Any CPU {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|Any CPU {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE 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,25 +0,0 @@
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,174 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Designs;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelApplyAll = "Glamourer.ApplyAll";
public const string LabelApplyAllOnce = "Glamourer.ApplyAllOnce";
public const string LabelApplyAllToCharacter = "Glamourer.ApplyAllToCharacter";
public const string LabelApplyAllOnceToCharacter = "Glamourer.ApplyAllOnceToCharacter";
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";
public const string LabelApplyByGuid = "Glamourer.ApplyByGuid";
public const string LabelApplyByGuidOnce = "Glamourer.ApplyByGuidOnce";
public const string LabelApplyByGuidToCharacter = "Glamourer.ApplyByGuidToCharacter";
public const string LabelApplyByGuidOnceToCharacter = "Glamourer.ApplyByGuidOnceToCharacter";
private readonly ActionProvider<string, string> _applyAllProvider;
private readonly ActionProvider<string, string> _applyAllOnceProvider;
private readonly ActionProvider<string, Character?> _applyAllToCharacterProvider;
private readonly ActionProvider<string, Character?> _applyAllOnceToCharacterProvider;
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;
private readonly ActionProvider<Guid, string> _applyByGuidProvider;
private readonly ActionProvider<Guid, string> _applyByGuidOnceProvider;
private readonly ActionProvider<Guid, Character?> _applyByGuidToCharacterProvider;
private readonly ActionProvider<Guid, Character?> _applyByGuidOnceToCharacterProvider;
public static ActionSubscriber<string, string> ApplyAllSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAll);
public static ActionSubscriber<string, string> ApplyAllOnceSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAllOnce);
public static ActionSubscriber<string, Character?> ApplyAllToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAllToCharacter);
public static ActionSubscriber<string, Character?> ApplyAllOnceToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAllOnceToCharacter);
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 static ActionSubscriber<Guid, string> ApplyByGuidSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuid);
public static ActionSubscriber<Guid, string> ApplyByGuidOnceSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuidOnce);
public static ActionSubscriber<Guid, Character?> ApplyByGuidToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuidToCharacter);
public static ActionSubscriber<Guid, Character?> ApplyByGuidOnceToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyByGuidOnceToCharacter);
public void ApplyAll(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0);
public void ApplyAllOnce(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0, true);
public void ApplyAllToCharacter(string base64, Character? character)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0);
public void ApplyAllOnceToCharacter(string base64, Character? character)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0, true);
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);
public void ApplyByGuid(Guid identifier, string characterName)
=> ApplyDesignByGuid(identifier, FindActors(characterName), 0, false);
public void ApplyByGuidOnce(Guid identifier, string characterName)
=> ApplyDesignByGuid(identifier, FindActors(characterName), 0, true);
public void ApplyByGuidToCharacter(Guid identifier, Character? character)
=> ApplyDesignByGuid(identifier, FindActors(character), 0, false);
public void ApplyByGuidOnceToCharacter(Guid identifier, Character? character)
=> ApplyDesignByGuid(identifier, FindActors(character), 0, true);
private void ApplyDesign(DesignBase? design, IEnumerable<ActorIdentifier> actors, byte version, uint lockCode, bool once = false)
{
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(state, design,
new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: lockCode, MergeLinks: true, ResetMaterials: !once && lockCode != 0));
state.Lock(lockCode);
}
}
}
private void ApplyDesignByGuid(Guid identifier, IEnumerable<ActorIdentifier> actors, uint lockCode, bool once)
=> ApplyDesign(_designManager.Designs.ByIdentifier(identifier), actors, DesignConverter.Version, lockCode, once);
}

View file

@ -1,17 +0,0 @@
using Dalamud.Plugin;
using Penumbra.Api.Helpers;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelGetDesignList = "Glamourer.GetDesignList";
private readonly FuncProvider<(string Name, Guid Identifier)[]> _getDesignListProvider;
public static FuncSubscriber<(string Name, Guid Identifier)[]> GetDesignListSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelGetDesignList);
public (string Name, Guid Identifier)[] GetDesignList()
=> _designManager.Designs.Select(x => (x.Name.Text, x.Identifier)).ToArray();
}

View file

@ -1,27 +0,0 @@
using Glamourer.Designs;
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, StateSource 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, ApplicationRules.AllButParameters(state))));
}
private void OnGPoseChanged(bool value)
=> _gPoseChangedProvider.Invoke(value);
}

View file

@ -1,46 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Designs;
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, ApplicationRules.All);
}
}

View file

@ -1,120 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.State;
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, StateSource.IpcFixed, 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, StateSource.IpcManual);
}
}
}
return ret;
}
}

View file

@ -1,105 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public enum GlamourerErrorCode
{
Success,
ActorNotFound,
ActorNotHuman,
ItemInvalid,
}
public const string LabelSetItem = "Glamourer.SetItem";
public const string LabelSetItemOnce = "Glamourer.SetItemOnce";
public const string LabelSetItemByActorName = "Glamourer.SetItemByActorName";
public const string LabelSetItemOnceByActorName = "Glamourer.SetItemOnceByActorName";
private readonly FuncProvider<Character?, byte, ulong, byte, uint, int> _setItemProvider;
private readonly FuncProvider<Character?, byte, ulong, byte, uint, int> _setItemOnceProvider;
private readonly FuncProvider<string, byte, ulong, byte, uint, int> _setItemByActorNameProvider;
private readonly FuncProvider<string, byte, ulong, byte, uint, int> _setItemOnceByActorNameProvider;
public static FuncSubscriber<Character?, byte, ulong, byte, uint, int> SetItemSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItem);
public static FuncSubscriber<Character?, byte, ulong, byte, uint, int> SetItemOnceSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItemOnce);
public static FuncSubscriber<string, byte, ulong, byte, uint, int> SetItemByActorNameSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItemByActorName);
public static FuncSubscriber<string, byte, ulong, byte, uint, int> SetItemOnceByActorNameSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItemOnceByActorName);
private GlamourerErrorCode SetItem(Character? character, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key, bool once)
{
if (itemId.Id == 0)
itemId = ItemManager.NothingId(slot);
var item = _items.Resolve(slot, itemId);
if (!item.Valid)
return GlamourerErrorCode.ItemInvalid;
var identifier = _actors.FromObject(character, false, false, false);
if (!identifier.IsValid)
return GlamourerErrorCode.ActorNotFound;
if (!_stateManager.TryGetValue(identifier, out var state))
{
_objects.Update();
var data = _objects[identifier];
if (!data.Valid || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state))
return GlamourerErrorCode.ActorNotFound;
}
if (!state.ModelData.IsHuman)
return GlamourerErrorCode.ActorNotHuman;
_stateManager.ChangeEquip(state, slot, item, stainId,
new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key));
return GlamourerErrorCode.Success;
}
private GlamourerErrorCode SetItemByActorName(string name, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key, bool once)
{
if (itemId.Id == 0)
itemId = ItemManager.NothingId(slot);
var item = _items.Resolve(slot, itemId);
if (!item.Valid)
return GlamourerErrorCode.ItemInvalid;
var found = false;
_objects.Update();
foreach (var identifier in FindActorsRevert(name).Distinct())
{
if (!_stateManager.TryGetValue(identifier, out var state))
{
var data = _objects[identifier];
if (!data.Valid || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state))
continue;
}
if (!state.ModelData.IsHuman)
return GlamourerErrorCode.ActorNotHuman;
_stateManager.ChangeEquip(state, slot, item, stainId,
new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key));
found = true;
}
return found ? GlamourerErrorCode.Success : GlamourerErrorCode.ActorNotFound;
}
}

View file

@ -1,188 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
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.GameData.Enums;
using Penumbra.String;
namespace Glamourer.Api;
public sealed partial class GlamourerIpc : IDisposable
{
public const int CurrentApiVersionMajor = 0;
public const int CurrentApiVersionMinor = 4;
private readonly StateManager _stateManager;
private readonly ObjectManager _objects;
private readonly ActorManager _actors;
private readonly DesignConverter _designConverter;
private readonly AutoDesignApplier _autoDesignApplier;
private readonly DesignManager _designManager;
private readonly ItemManager _items;
public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorManager actors,
DesignConverter designConverter, StateChanged stateChangedEvent, GPoseService gPose, AutoDesignApplier autoDesignApplier,
DesignManager designManager, ItemManager items)
{
_stateManager = stateManager;
_objects = objects;
_actors = actors;
_designConverter = designConverter;
_autoDesignApplier = autoDesignApplier;
_items = items;
_gPose = gPose;
_stateChangedEvent = stateChangedEvent;
_designManager = designManager;
_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);
_applyAllOnceProvider = new ActionProvider<string, string>(pi, LabelApplyAllOnce, ApplyAllOnce);
_applyAllToCharacterProvider = new ActionProvider<string, Character?>(pi, LabelApplyAllToCharacter, ApplyAllToCharacter);
_applyAllOnceToCharacterProvider = new ActionProvider<string, Character?>(pi, LabelApplyAllOnceToCharacter, ApplyAllOnceToCharacter);
_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);
_applyByGuidProvider = new ActionProvider<Guid, string>(pi, LabelApplyByGuid, ApplyByGuid);
_applyByGuidOnceProvider = new ActionProvider<Guid, string>(pi, LabelApplyByGuidOnce, ApplyByGuidOnce);
_applyByGuidToCharacterProvider = new ActionProvider<Guid, Character?>(pi, LabelApplyByGuidToCharacter, ApplyByGuidToCharacter);
_applyByGuidOnceToCharacterProvider =
new ActionProvider<Guid, Character?>(pi, LabelApplyByGuidOnceToCharacter, ApplyByGuidOnceToCharacter);
_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);
_setItemProvider = new FuncProvider<Character?, byte, ulong, byte, uint, int>(pi, LabelSetItem,
(idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key, false));
_setItemOnceProvider = new FuncProvider<Character?, byte, ulong, byte, uint, int>(pi, LabelSetItemOnce,
(idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key, true));
_setItemByActorNameProvider = new FuncProvider<string, byte, ulong, byte, uint, int>(pi, LabelSetItemByActorName,
(name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key, false));
_setItemOnceByActorNameProvider = new FuncProvider<string, byte, ulong, byte, uint, int>(pi, LabelSetItemOnceByActorName,
(name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key, true));
_stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc);
_gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc);
_getDesignListProvider = new FuncProvider<(string Name, Guid Identifier)[]>(pi, LabelGetDesignList, GetDesignList);
}
public void Dispose()
{
_apiVersionProvider.Dispose();
_apiVersionsProvider.Dispose();
_getAllCustomizationProvider.Dispose();
_getAllCustomizationFromCharacterProvider.Dispose();
_applyAllProvider.Dispose();
_applyAllOnceProvider.Dispose();
_applyAllToCharacterProvider.Dispose();
_applyAllOnceToCharacterProvider.Dispose();
_applyOnlyEquipmentProvider.Dispose();
_applyOnlyEquipmentToCharacterProvider.Dispose();
_applyOnlyCustomizationProvider.Dispose();
_applyOnlyCustomizationToCharacterProvider.Dispose();
_applyAllProviderLock.Dispose();
_applyAllToCharacterProviderLock.Dispose();
_applyOnlyEquipmentProviderLock.Dispose();
_applyOnlyEquipmentToCharacterProviderLock.Dispose();
_applyOnlyCustomizationProviderLock.Dispose();
_applyOnlyCustomizationToCharacterProviderLock.Dispose();
_applyByGuidProvider.Dispose();
_applyByGuidOnceProvider.Dispose();
_applyByGuidToCharacterProvider.Dispose();
_applyByGuidOnceToCharacterProvider.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();
_getDesignListProvider.Dispose();
_setItemProvider.Dispose();
_setItemOnceProvider.Dispose();
_setItemByActorNameProvider.Dispose();
_setItemOnceByActorNameProvider.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.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

@ -1,4 +1,5 @@
using Glamourer.Designs; using Glamourer.Api.Enums;
using Glamourer.Designs;
using Glamourer.GameData; using Glamourer.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -28,8 +29,7 @@ public static class ApplicationTypeExtensions
(ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."), (ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."),
]; ];
public static (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat( public static ApplicationCollection Collection(this ApplicationType type)
this ApplicationType type, IDesignStandIn designStandIn)
{ {
var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0) var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0)
| (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0) | (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0)
@ -37,16 +37,22 @@ public static class ApplicationTypeExtensions
| (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0); | (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0);
var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0; var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0;
var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0; var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0;
var crestFlag = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0; var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
var metaFlag = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState : 0) var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.EarState : 0)
| (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0) | (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0)
| (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0); | (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0);
var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0;
if (designStandIn is not DesignBase design) return new ApplicationCollection(equipFlags, bonusFlags, customizeFlags, crestFlags, parameterFlags, metaFlags);
return (equipFlags, customizeFlags, crestFlag, parameterFlags, metaFlag); }
return (equipFlags & design!.ApplyEquip, customizeFlags & design.ApplyCustomize, crestFlag & design.ApplyCrest, public static ApplicationCollection ApplyWhat(this ApplicationType type, IDesignStandIn designStandIn)
parameterFlags & design.ApplyParameters, metaFlag & design.ApplyMeta); {
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 WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;

View file

@ -1,9 +1,9 @@
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Designs.Special; using Glamourer.Designs.Special;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Interop.Structs;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Automation; namespace Glamourer.Automation;
@ -61,6 +61,6 @@ public class AutoDesign
return ret; return ret;
} }
public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat() public ApplicationCollection ApplyWhat()
=> Type.ApplyWhat(Design); => Type.ApplyWhat(Design);
} }

View file

@ -4,34 +4,35 @@ using Glamourer.Designs;
using Glamourer.Designs.Links; using Glamourer.Designs.Links;
using Glamourer.Events; using Glamourer.Events;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.Interop.Structs; using Glamourer.Interop.Material;
using Glamourer.State; using Glamourer.State;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Automation; namespace Glamourer.Automation;
public sealed class AutoDesignApplier : IDisposable public sealed class AutoDesignApplier : IDisposable
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly AutoDesignManager _manager; private readonly AutoDesignManager _manager;
private readonly StateManager _state; private readonly StateManager _state;
private readonly JobService _jobs; private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset; private readonly EquippedGearset _equippedGearset;
private readonly ActorManager _actors; private readonly ActorManager _actors;
private readonly AutomationChanged _event; private readonly AutomationChanged _event;
private readonly ObjectManager _objects; private readonly ActorObjectManager _objects;
private readonly WeaponLoading _weapons; private readonly WeaponLoading _weapons;
private readonly HumanModelList _humans; private readonly HumanModelList _humans;
private readonly DesignMerger _designMerger; private readonly DesignMerger _designMerger;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly JobChangeState _jobChangeState; private readonly JobChangeState _jobChangeState;
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors, public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors,
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, AutomationChanged @event, ActorObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState,
EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState) EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState)
{ {
_config = config; _config = config;
@ -53,6 +54,15 @@ public sealed class AutoDesignApplier : IDisposable
_equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.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() public void Dispose()
{ {
_weapons.Unsubscribe(OnWeaponLoading); _weapons.Unsubscribe(OnWeaponLoading);
@ -75,7 +85,7 @@ public sealed class AutoDesignApplier : IDisposable
{ {
case EquipSlot.MainHand: case EquipSlot.MainHand:
{ {
if (_jobChangeState.TryGetValue(current.Type, out var data)) if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{ {
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}."); $"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}.");
@ -87,7 +97,7 @@ public sealed class AutoDesignApplier : IDisposable
} }
case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand(): case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand():
{ {
if (_jobChangeState.TryGetValue(current.Type, out var data)) if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{ {
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}."); $"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}.");
@ -143,16 +153,15 @@ public sealed class AutoDesignApplier : IDisposable
if (newSet is not { Enabled: true }) if (newSet is not { Enabled: true })
return; return;
_objects.Update();
foreach (var id in newSet.Identifiers) foreach (var id in newSet.Identifiers)
{ {
if (_objects.TryGetValue(id, out var data)) if (_objects.TryGetValue(id, out var data))
{ {
if (_state.GetOrCreate(id, data.Objects[0], out var state)) if (_state.GetOrCreate(id, data.Objects[0], out var state))
{ {
Reduce(data.Objects[0], state, newSet, false, false); Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
foreach (var actor in data.Objects) foreach (var actor in data.Objects)
_state.ReapplyState(actor, StateSource.Fixed); _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
} }
} }
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data)) else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
@ -162,8 +171,8 @@ public sealed class AutoDesignApplier : IDisposable
var specificId = actor.GetIdentifier(_actors); var specificId = actor.GetIdentifier(_actors);
if (_state.GetOrCreate(specificId, actor, out var state)) if (_state.GetOrCreate(specificId, actor, out var state))
{ {
Reduce(actor, state, newSet, false, false); Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
_state.ReapplyState(actor, StateSource.Fixed); _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
} }
} }
} }
@ -202,7 +211,7 @@ public sealed class AutoDesignApplier : IDisposable
return; return;
} }
if (!_state.TryGetValue(id, out var state)) if (!_state.GetOrCreate(actor, out var state))
return; return;
if (oldJob.Id == newJob.Id && state.LastJob == newJob.Id) if (oldJob.Id == newJob.Id && state.LastJob == newJob.Id)
@ -210,19 +219,21 @@ public sealed class AutoDesignApplier : IDisposable
var respectManual = state.LastJob == newJob.Id; var respectManual = state.LastJob == newJob.Id;
state.LastJob = actor.Job; state.LastJob = actor.Job;
Reduce(actor, state, set, respectManual, true); Reduce(actor, state, set, respectManual, true, true, out var forcedRedraw);
_state.ReapplyState(actor, StateSource.Fixed); _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) if (!_config.EnableAutoDesigns)
return; return;
if (!GetPlayerSet(identifier, out var set)) if (reset)
return; _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) public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state)
@ -230,9 +241,6 @@ public sealed class AutoDesignApplier : IDisposable
AutoDesignSet set; AutoDesignSet set;
if (!_state.TryGetValue(identifier, out state)) if (!_state.TryGetValue(identifier, out state))
{ {
if (!_config.EnableAutoDesigns)
return false;
if (!GetPlayerSet(identifier, out set!)) if (!GetPlayerSet(identifier, out set!))
return false; return false;
@ -249,41 +257,83 @@ public sealed class AutoDesignApplier : IDisposable
var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange; var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange;
if (!respectManual) if (!respectManual)
_state.ResetState(state, StateSource.Game); _state.ResetState(state, StateSource.Game);
Reduce(actor, state, set, respectManual, false); Reduce(actor, state, set, respectManual, false, false, out _);
return true; 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)
{ {
if (set.BaseState is AutoDesignSet.Base.Game) if (set.BaseState is AutoDesignSet.Base.Game)
{
_state.ResetStateFixed(state, respectManual); _state.ResetStateFixed(state, respectManual);
}
else if (!respectManual) else if (!respectManual)
{
state.Sources.RemoveFixedDesignSources(); 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 (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) forcedRedraw = false;
if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId))
return;
if (actor.IsTransformed)
return; return;
var mergedDesign = _designMerger.Merge( var mergedDesign = _designMerger.Merge(
set.Designs.Where(d => d.IsActive(actor)).SelectMany(d => d.Design.AllLinks.Select(l => (l.Design, l.Flags & d.Type))), 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); state.ModelData.Customize, state.BaseData, true, _config.AlwaysApplyAssociatedMods);
_state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false, false, set.BaseState is AutoDesignSet.Base.Game));
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 afterward. </summary> /// <summary> Get world-specific first and all-world afterward. </summary>
private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set) private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set)
{ {
if (!_config.EnableAutoDesigns)
{
set = null;
return false;
}
switch (identifier.Type) switch (identifier.Type)
{ {
case IdentifierType.Player: case IdentifierType.Player:
if (_manager.EnabledSets.TryGetValue(identifier, out set)) if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true; return true;
identifier = _actors.CreatePlayer(identifier.PlayerName, ushort.MaxValue); identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld);
return _manager.EnabledSets.TryGetValue(identifier, out set); return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Retainer: case IdentifierType.Retainer:
case IdentifierType.Npc: case IdentifierType.Npc:
return _manager.EnabledSets.TryGetValue(identifier, out set); return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Owned: case IdentifierType.Owned:
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); identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
return _manager.EnabledSets.TryGetValue(identifier, out set); return _manager.EnabledSets.TryGetValue(identifier, out set);
default: default:
@ -308,10 +358,10 @@ public sealed class AutoDesignApplier : IDisposable
var respectManual = prior == id; var respectManual = prior == id;
NewGearsetId = id; NewGearsetId = id;
Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob); Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, prior == id, out var forcedRedraw);
NewGearsetId = -1; NewGearsetId = -1;
foreach (var actor in data.Objects) foreach (var actor in data.Objects)
_state.ReapplyState(actor, StateSource.Fixed); _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
} }
public static unsafe bool CheckGearset(short check) public static unsafe bool CheckGearset(short check)

View file

@ -1,6 +1,7 @@
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Designs.Special; using Glamourer.Designs.Special;
using Glamourer.Events; using Glamourer.Events;
using Glamourer.Interop; using Glamourer.Interop;
@ -9,6 +10,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -28,6 +30,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
private readonly AutomationChanged _event; private readonly AutomationChanged _event;
private readonly DesignChanged _designEvent; private readonly DesignChanged _designEvent;
private readonly RandomDesignGenerator _randomDesigns; private readonly RandomDesignGenerator _randomDesigns;
private readonly QuickSelectedDesign _quickSelectedDesign;
private readonly List<AutoDesignSet> _data = []; private readonly List<AutoDesignSet> _data = [];
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = []; private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = [];
@ -36,15 +39,17 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
=> _enabled; => _enabled;
public AutoDesignManager(JobService jobs, ActorManager actors, SaveService saveService, DesignManager designs, AutomationChanged @event, public AutoDesignManager(JobService jobs, ActorManager actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent, RandomDesignGenerator randomDesigns) FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent, RandomDesignGenerator randomDesigns,
QuickSelectedDesign quickSelectedDesign)
{ {
_jobs = jobs; _jobs = jobs;
_actors = actors; _actors = actors;
_saveService = saveService; _saveService = saveService;
_designs = designs; _designs = designs;
_event = @event; _event = @event;
_designEvent = designEvent; _designEvent = designEvent;
_randomDesigns = randomDesigns; _randomDesigns = randomDesigns;
_quickSelectedDesign = quickSelectedDesign;
_designEvent.Subscribe(OnDesignChange, DesignChanged.Priority.AutoDesignManager); _designEvent.Subscribe(OnDesignChange, DesignChanged.Priority.AutoDesignManager);
Load(); Load();
migrator.ConsumeMigratedData(_actors, fileSystem, this); migrator.ConsumeMigratedData(_actors, fileSystem, this);
@ -230,6 +235,22 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
_event.Invoke(AutomationChanged.Type.ChangedBase, set, (old, newBase)); _event.Invoke(AutomationChanged.Type.ChangedBase, set, (old, newBase));
} }
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) public void AddDesign(AutoDesignSet set, IDesignStandIn design)
{ {
var newDesign = new AutoDesign() var newDesign = new AutoDesign()
@ -440,8 +461,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
var set = new AutoDesignSet(name, group) var set = new AutoDesignSet(name, group)
{ {
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false, Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current, ResetTemporarySettings = obj["ResetTemporarySettings"]?.ToObject<bool>() ?? false,
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current,
}; };
if (set.Enabled) if (set.Enabled)
@ -486,6 +508,10 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
{ {
design = new RandomDesign(_randomDesigns); design = new RandomDesign(_randomDesigns);
} }
else if (designIdentifier is QuickSelectedDesign.SerializedName)
{
design = _quickSelectedDesign;
}
else else
{ {
if (designIdentifier.Length == 0) if (designIdentifier.Length == 0)
@ -562,12 +588,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
IdentifierType.Player => true, IdentifierType.Player => true,
IdentifierType.Retainer => true, IdentifierType.Retainer => true,
IdentifierType.Npc => true, IdentifierType.Npc => true,
IdentifierType.Owned => true,
_ => false, _ => false,
}; };
if (!validType) if (!validType)
{ {
group = Array.Empty<ActorIdentifier>(); group = [];
return false; return false;
} }
@ -593,8 +620,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
? ActorIdentifier.RetainerType.Mannequin ? ActorIdentifier.RetainerType.Mannequin
: ActorIdentifier.RetainerType.Bell).CreatePermanent(), : ActorIdentifier.RetainerType.Bell).CreatePermanent(),
], ],
IdentifierType.Npc => CreateNpcs(_actors, identifier), IdentifierType.Npc => CreateNpcs(_actors, identifier),
_ => [], IdentifierType.Owned => CreateNpcs(_actors, identifier),
_ => [],
}; };
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
@ -608,12 +636,11 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
}; };
return table.Where(kvp => kvp.Value == name) return table.Where(kvp => kvp.Value == name)
.Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id,
identifier.Kind, identifier.Kind, kvp.Key)).ToArray();
kvp.Key)).ToArray();
} }
} }
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) if (type is not DesignChanged.Type.Deleted)
return; return;

View file

@ -10,7 +10,8 @@ public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List<Auto
public string Name = name; public string Name = name;
public ActorIdentifier[] Identifiers = identifiers; public ActorIdentifier[] Identifiers = identifiers;
public bool Enabled; public bool Enabled;
public Base BaseState = Base.Current; public Base BaseState = Base.Current;
public bool ResetTemporarySettings = false;
public JObject Serialize() public JObject Serialize()
{ {
@ -20,11 +21,12 @@ public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List<Auto
return new JObject() return new JObject()
{ {
["Name"] = Name, ["Name"] = Name,
["Identifier"] = Identifiers[0].ToJson(), ["Identifier"] = Identifiers[0].ToJson(),
["Enabled"] = Enabled, ["Enabled"] = Enabled,
["BaseState"] = BaseState.ToString(), ["BaseState"] = BaseState.ToString(),
["Designs"] = list, ["ResetTemporarySettings"] = ResetTemporarySettings.ToString(),
["Designs"] = list,
}; };
} }

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Interop; using Glamourer.Interop;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;

View file

@ -1,56 +1,91 @@
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Services; using Glamourer.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using OtterGui.Widgets; using OtterGui.Widgets;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Glamourer; 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 class Configuration : IPluginConfiguration, ISavable
{ {
[JsonIgnore] [JsonIgnore]
public readonly EphemeralConfig Ephemeral; public readonly EphemeralConfig Ephemeral;
public bool UseRestrictedGearProtection { get; set; } = false; public bool AttachToPcp { get; set; } = true;
public bool OpenFoldersByDefault { get; set; } = false; public bool UseRestrictedGearProtection { get; set; } = false;
public bool AutoRedrawEquipOnChanges { get; set; } = false; public bool OpenFoldersByDefault { get; set; } = false;
public bool EnableAutoDesigns { get; set; } = true; public bool AutoRedrawEquipOnChanges { get; set; } = false;
public bool HideApplyCheckmarks { get; set; } = false; public bool EnableAutoDesigns { get; set; } = true;
public bool SmallEquip { get; set; } = false; public bool HideApplyCheckmarks { get; set; } = false;
public bool UnlockedItemMode { get; set; } = false; public bool SmallEquip { get; set; } = false;
public byte DisableFestivals { get; set; } = 1; public bool UnlockedItemMode { get; set; } = false;
public bool EnableGameContextMenu { get; set; } = true; public byte DisableFestivals { get; set; } = 1;
public bool HideWindowInCutscene { get; set; } = false; public bool EnableGameContextMenu { get; set; } = true;
public bool ShowAutomationSetEditing { get; set; } = true; public bool HideWindowInCutscene { get; set; } = false;
public bool ShowAllAutomatedApplicationRules { get; set; } = true; public bool ShowAutomationSetEditing { get; set; } = true;
public bool ShowUnlockedItemWarnings { get; set; } = true; public bool ShowAllAutomatedApplicationRules { get; set; } = true;
public bool RevertManualChangesOnZoneChange { get; set; } = false; public bool ShowUnlockedItemWarnings { get; set; } = true;
public bool ShowQuickBarInTabs { get; set; } = true; public bool RevertManualChangesOnZoneChange { get; set; } = false;
public bool OpenWindowAtStart { get; set; } = false; public bool ShowQuickBarInTabs { get; set; } = true;
public bool ShowWindowWhenUiHidden { get; set; } = false; public bool OpenWindowAtStart { get; set; } = false;
public bool UseAdvancedParameters { get; set; } = true; public bool ShowWindowWhenUiHidden { get; set; } = false;
public bool UseAdvancedDyes { get; set; } = true; public bool KeepAdvancedDyesAttached { get; set; } = true;
public bool KeepAdvancedDyesAttached { get; set; } = true; public bool ShowPalettePlusImport { get; set; } = true;
public bool ShowPalettePlusImport { get; set; } = true; public bool UseFloatForColors { get; set; } = true;
public bool UseFloatForColors { get; set; } = true; public bool UseRgbForColors { get; set; } = true;
public bool UseRgbForColors { get; set; } = true; public bool ShowColorConfig { get; set; } = true;
public bool ShowColorConfig { get; set; } = true; public bool ChangeEntireItem { get; set; } = false;
public bool ChangeEntireItem { get; set; } = false; public bool AlwaysApplyAssociatedMods { get; set; } = true;
public bool AlwaysApplyAssociatedMods { get; set; } = false; public bool UseTemporarySettings { get; set; } = true;
public bool AllowDoubleClickToApply { get; set; } = false; public bool AllowDoubleClickToApply { get; set; } = false;
public ModifiableHotkey ToggleQuickDesignBar { get; set; } = new(VirtualKey.NO_KEY); public bool RespectManualOnAutomationUpdate { get; set; } = false;
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool PreventRandomRepeats { get; set; } = false;
public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; 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; } = public QdbButtons QdbButtons { get; set; } =
QdbButtons.ApplyDesign | QdbButtons.RevertAll | QdbButtons.RevertAutomation | QdbButtons.RevertAdvanced; QdbButtons.ApplyDesign | QdbButtons.RevertAll | QdbButtons.RevertAutomation | QdbButtons.RevertAdvancedDyes;
[JsonConverter(typeof(SortModeConverter))] [JsonConverter(typeof(SortModeConverter))]
[JsonProperty(Order = int.MaxValue)] [JsonProperty(Order = int.MaxValue)]
@ -127,10 +162,10 @@ public class Configuration : IPluginConfiguration, ISavable
public static class Constants public static class Constants
{ {
public const int CurrentVersion = 6; public const int CurrentVersion = 8;
public static readonly ISortMode<Design>[] ValidSortModes = public static readonly ISortMode<Design>[] ValidSortModes =
{ [
ISortMode<Design>.FoldersFirst, ISortMode<Design>.FoldersFirst,
ISortMode<Design>.Lexicographical, ISortMode<Design>.Lexicographical,
new DesignFileSystem.CreationDate(), new DesignFileSystem.CreationDate(),
@ -143,7 +178,7 @@ public class Configuration : IPluginConfiguration, ISavable
ISortMode<Design>.InverseFoldersLast, ISortMode<Design>.InverseFoldersLast,
ISortMode<Design>.InternalOrder, ISortMode<Design>.InternalOrder,
ISortMode<Design>.InverseInternalOrder, ISortMode<Design>.InverseInternalOrder,
}; ];
} }
/// <summary> Convert SortMode Types to their name. </summary> /// <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

@ -1,19 +1,14 @@
using Glamourer.GameData; using Glamourer.Api.Enums;
using Glamourer.GameData;
using Glamourer.State; using Glamourer.State;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
namespace Glamourer.Designs; namespace Glamourer.Designs;
public readonly struct ApplicationRules( public readonly struct ApplicationRules(ApplicationCollection application, bool materials)
EquipFlag equip,
CustomizeFlag customize,
CrestFlag crest,
CustomizeParameterFlag parameters,
MetaFlag meta)
{ {
public static readonly ApplicationRules All = new(EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, public static readonly ApplicationRules All = new(ApplicationCollection.All, true);
CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All);
public static ApplicationRules FromModifiers(ActorState state) public static ApplicationRules FromModifiers(ActorState state)
=> FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); => FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
@ -22,53 +17,45 @@ public readonly struct ApplicationRules(
=> NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); => NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
public static ApplicationRules AllButParameters(ActorState state) public static ApplicationRules AllButParameters(ActorState state)
=> new(All.Equip, All.Customize, All.Crest, ComputeParameters(state.ModelData, state.BaseData, All.Parameters), All.Meta); => new(ApplicationCollection.All with { Parameters = ComputeParameters(state.ModelData, state.BaseData, All.Parameters) }, true);
public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift) public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift)
=> new(ctrl || !shift ? EquipFlagExtensions.All : 0, {
!ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0, var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
0, var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
0, var visor = equip != 0 ? MetaFlag.VisorState : 0;
ctrl || !shift ? 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) public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift)
{ {
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0; var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0; var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
var bonus = equip == 0 ? 0 : BonusExtensions.All;
var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant; var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant;
var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All; var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All;
var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0; var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0;
if (equip != 0) if (equip != 0)
meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState; meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState;
return new ApplicationRules(equip, customize, crest, ComputeParameters(state.ModelData, state.BaseData, parameters), meta); 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) public void Apply(DesignBase design)
{ => design.Application = application;
design.ApplyEquip = Equip;
design.ApplyCustomize = Customize;
design.ApplyCrest = Crest;
design.ApplyParameters = Parameters;
design.ApplyMeta = Meta;
}
public EquipFlag Equip public EquipFlag Equip
=> equip & EquipFlagExtensions.All; => application.Equip & EquipFlagExtensions.All;
public CustomizeFlag Customize
=> customize & CustomizeFlagExtensions.AllRelevant;
public CrestFlag Crest
=> crest & CrestExtensions.AllRelevant;
public CustomizeParameterFlag Parameters public CustomizeParameterFlag Parameters
=> parameters & CustomizeParameterExtensions.All; => application.Parameters & CustomizeParameterExtensions.All;
public MetaFlag Meta public bool Materials
=> meta & MetaExtensions.All; => materials;
public static CustomizeParameterFlag ComputeParameters(in DesignData model, in DesignData game, private static CustomizeParameterFlag ComputeParameters(in DesignData model, in DesignData game,
CustomizeParameterFlag baseFlags = CustomizeParameterExtensions.All) CustomizeParameterFlag baseFlags = CustomizeParameterExtensions.All)
{ {
foreach (var flag in baseFlags.Iterate()) foreach (var flag in baseFlags.Iterate())

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Glamourer.Automation; using Glamourer.Automation;
using Glamourer.Designs.Links; using Glamourer.Designs.Links;
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
@ -8,6 +8,8 @@ using Glamourer.State;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Structs;
using Notification = OtterGui.Classes.Notification;
namespace Glamourer.Designs; namespace Glamourer.Designs;
@ -26,32 +28,40 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
internal Design(Design other) internal Design(Design other)
: base(other) : base(other)
{ {
Tags = [.. other.Tags]; Tags = [.. other.Tags];
Description = other.Description; Description = other.Description;
QuickDesign = other.QuickDesign; QuickDesign = other.QuickDesign;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods); ForcedRedraw = other.ForcedRedraw;
ResetAdvancedDyes = other.ResetAdvancedDyes;
ResetTemporarySettings = other.ResetTemporarySettings;
Color = other.Color;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
Links = Links.Clone();
} }
// Metadata // Metadata
public new const int FileVersion = 1; public new const int FileVersion = 2;
public Guid Identifier { get; internal init; } public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; } public DateTimeOffset CreationDate { get; internal init; }
public DateTimeOffset LastEdit { get; internal set; } public DateTimeOffset LastEdit { get; internal set; }
public LowerString Name { get; internal set; } = LowerString.Empty; public LowerString Name { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty; public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = []; public string[] Tags { get; internal set; } = [];
public int Index { get; internal set; } public int Index { get; internal set; }
public bool QuickDesign { get; internal set; } = true; public bool ForcedRedraw { get; internal set; }
public string Color { get; internal set; } = string.Empty; public bool ResetAdvancedDyes { get; internal set; }
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = []; public bool ResetTemporarySettings { get; internal set; }
public LinkContainer Links { get; private 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 public string Incognito
=> Identifier.ToString()[..8]; => Identifier.ToString()[..8];
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags)> AllLinks public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
=> LinkContainer.GetAllLinks(this).Select(t => ((IDesignStandIn)t.Link.Link, t.Link.Type)); => LinkContainer.GetAllLinks(this).Select(t => ((IDesignStandIn)t.Link.Link, t.Link.Type, JobFlag.All));
#endregion #endregion
@ -90,24 +100,28 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
public new JObject JsonSerialize() public new JObject JsonSerialize()
{ {
var ret = new JObject() var ret = new JObject
{ {
["FileVersion"] = FileVersion, ["FileVersion"] = FileVersion,
["Identifier"] = Identifier, ["Identifier"] = Identifier,
["CreationDate"] = CreationDate, ["CreationDate"] = CreationDate,
["LastEdit"] = LastEdit, ["LastEdit"] = LastEdit,
["Name"] = Name.Text, ["Name"] = Name.Text,
["Description"] = Description, ["Description"] = Description,
["Color"] = Color, ["ForcedRedraw"] = ForcedRedraw,
["QuickDesign"] = QuickDesign, ["ResetAdvancedDyes"] = ResetAdvancedDyes,
["Tags"] = JArray.FromObject(Tags), ["ResetTemporarySettings"] = ResetTemporarySettings,
["WriteProtected"] = WriteProtected(), ["Color"] = Color,
["Equipment"] = SerializeEquipment(), ["QuickDesign"] = QuickDesign,
["Customize"] = SerializeCustomize(), ["Tags"] = JArray.FromObject(Tags),
["Parameters"] = SerializeParameters(), ["WriteProtected"] = WriteProtected(),
["Materials"] = SerializeMaterials(), ["Equipment"] = SerializeEquipment(),
["Mods"] = SerializeMods(), ["Bonus"] = SerializeBonusItems(),
["Links"] = Links.Serialize(), ["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
["Mods"] = SerializeMods(),
["Links"] = Links.Serialize(),
}; };
return ret; return ret;
} }
@ -117,12 +131,17 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
var ret = new JArray(); var ret = new JArray();
foreach (var (mod, settings) in AssociatedMods) foreach (var (mod, settings) in AssociatedMods)
{ {
var obj = new JObject() var obj = new JObject
{ {
["Name"] = mod.Name, ["Name"] = mod.Name,
["Directory"] = mod.DirectoryName, ["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) if (settings.Enabled)
{ {
obj["Priority"] = settings.Priority; obj["Priority"] = settings.Priority;
@ -139,17 +158,83 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
#region Deserialization #region Deserialization
public static Design LoadDesign(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json) public static Design LoadDesign(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader,
JObject json)
{ {
var version = json["FileVersion"]?.ToObject<int>() ?? 0; var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch return version switch
{ {
FileVersion => LoadDesignV1(customizations, items, linkLoader, 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."), _ => throw new Exception("The design to be loaded has no valid Version."),
}; };
} }
private static Design LoadDesignV1(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, 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)
{
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))
{
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 creationDate = json["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
@ -168,16 +253,20 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
design.SetWriteProtected(json["WriteProtected"]?.ToObject<bool>() ?? false); design.SetWriteProtected(json["WriteProtected"]?.ToObject<bool>() ?? false);
LoadCustomize(customizations, json["Customize"], design, design.Name, true, false); LoadCustomize(customizations, json["Customize"], design, design.Name, true, false);
LoadEquip(items, json["Equipment"], design, design.Name, true); LoadEquip(items, json["Equipment"], design, design.Name, true);
LoadBonus(items, design, json["Bonus"]);
LoadMods(json["Mods"], design); LoadMods(json["Mods"], design);
LoadParameters(json["Parameters"], design, design.Name); LoadParameters(json["Parameters"], design, design.Name);
LoadMaterials(json["Materials"], design, design.Name); LoadMaterials(json["Materials"], design, design.Name);
LoadLinks(linkLoader, json["Links"], design); LoadLinks(linkLoader, json["Links"], design);
design.Color = json["Color"]?.ToObject<string>() ?? string.Empty; 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; return design;
static string[] ParseTags(JObject json) static string[] ParseTags(JObject json)
{ {
var tags = json["Tags"]?.ToObject<string[]>() ?? Array.Empty<string>(); var tags = json["Tags"]?.ToObject<string[]>() ?? [];
return tags.OrderBy(t => t).Distinct().ToArray(); return tags.OrderBy(t => t).Distinct().ToArray();
} }
} }
@ -191,19 +280,22 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
{ {
var name = tok["Name"]?.ToObject<string>(); var name = tok["Name"]?.ToObject<string>();
var directory = tok["Directory"]?.ToObject<string>(); var directory = tok["Directory"]?.ToObject<string>();
var enabled = tok["Enabled"]?.ToObject<bool>(); var enabled = tok["Enabled"]?.ToObject<bool>() ?? false;
if (name == null || directory == null || enabled == null) if (name == null || directory == null)
{ {
Glamourer.Messager.NotificationMessage("The loaded design contains an invalid mod, skipped.", NotificationType.Warning); Glamourer.Messager.NotificationMessage("The loaded design contains an invalid mod, skipped.", NotificationType.Warning);
continue; continue;
} }
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, string[]>>() ?? new Dictionary<string, string[]>(); var forceInherit = tok["Inherit"]?.ToObject<bool>() ?? false;
var settings = new SortedList<string, IList<string>>(settingsDict.Count); 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) foreach (var (key, value) in settingsDict)
settings.Add(key, value); settings.Add(key, value);
var priority = tok["Priority"]?.ToObject<int>() ?? 0; 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, forceInherit, removeSetting)))
Glamourer.Messager.NotificationMessage("The loaded design contains a mod more than once, skipped.", NotificationType.Warning); Glamourer.Messager.NotificationMessage("The loaded design contains a mod more than once, skipped.", NotificationType.Warning);
} }
} }
@ -222,10 +314,10 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
if (array == null) if (array == null)
return; return;
foreach (var obj in array.OfType<JObject>()) foreach (var jObj in array.OfType<JObject>())
{ {
var identifier = obj["Design"]?.ToObject<Guid>() ?? throw new ArgumentNullException("Design"); var identifier = jObj["Design"]?.ToObject<Guid>() ?? throw new ArgumentNullException(nameof(design));
var type = (ApplicationType)(obj["Type"]?.ToObject<uint>() ?? 0); var type = (ApplicationType)(jObj["Type"]?.ToObject<uint>() ?? 0);
linkLoader.AddObject(design, new LinkData(identifier, type, order)); linkLoader.AddObject(design, new LinkData(identifier, type, order));
} }
} }

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
using Glamourer.Services; using Glamourer.Services;
@ -40,25 +40,23 @@ public class DesignBase
} }
/// <summary> Used when importing .cma or .chara files. </summary> /// <summary> Used when importing .cma or .chara files. </summary>
internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags) internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags,
BonusItemFlag bonusFlags)
{ {
_designData = designData; _designData = designData;
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
ApplyEquip = equipFlags & EquipFlagExtensions.All; Application.Equip = equipFlags & EquipFlagExtensions.All;
ApplyMeta = 0; Application.BonusItem = bonusFlags & BonusExtensions.All;
CustomizeSet = SetCustomizationSet(customize); Application.Meta = 0;
CustomizeSet = SetCustomizationSet(customize);
} }
internal DesignBase(DesignBase clone) internal DesignBase(DesignBase clone)
{ {
_designData = clone._designData; _designData = clone._designData;
_materials = clone._materials.Clone(); _materials = clone._materials.Clone();
CustomizeSet = clone.CustomizeSet; CustomizeSet = clone.CustomizeSet;
ApplyCustomize = clone.ApplyCustomizeRaw; Application = clone.Application.CloneSecure();
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
ApplyParameters = clone.ApplyParameters & CustomizeParameterExtensions.All;
ApplyCrest = clone.ApplyCrest & CrestExtensions.All;
ApplyMeta = clone.ApplyMeta & MetaExtensions.All;
} }
/// <summary> Ensure that the customization set is updated when the design data changes. </summary> /// <summary> Ensure that the customization set is updated when the design data changes. </summary>
@ -70,27 +68,20 @@ public class DesignBase
#region Application Data #region Application Data
private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant; public CustomizeSet CustomizeSet { get; private set; }
public CustomizeSet CustomizeSet { get; private set; }
public CustomizeParameterFlag ApplyParameters { get; internal set; } public ApplicationCollection Application = ApplicationCollection.Default;
internal CustomizeFlag ApplyCustomize internal CustomizeFlag ApplyCustomize
{ {
get => _applyCustomize.FixApplication(CustomizeSet); get => Application.Customize.FixApplication(CustomizeSet);
set => _applyCustomize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType; set => Application.Customize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType;
} }
internal CustomizeFlag ApplyCustomizeExcludingBodyType internal CustomizeFlag ApplyCustomizeExcludingBodyType
=> _applyCustomize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType; => Application.Customize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType;
internal CustomizeFlag ApplyCustomizeRaw private bool _writeProtected;
=> _applyCustomize;
internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState;
private bool _writeProtected;
public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize)
{ {
@ -103,18 +94,18 @@ public class DesignBase
} }
public bool DoApplyMeta(MetaIndex index) public bool DoApplyMeta(MetaIndex index)
=> ApplyMeta.HasFlag(index.ToFlag()); => Application.Meta.HasFlag(index.ToFlag());
public bool WriteProtected() public bool WriteProtected()
=> _writeProtected; => _writeProtected;
public bool SetApplyMeta(MetaIndex index, bool value) public bool SetApplyMeta(MetaIndex index, bool value)
{ {
var newFlag = value ? ApplyMeta | index.ToFlag() : ApplyMeta & ~index.ToFlag(); var newFlag = value ? Application.Meta | index.ToFlag() : Application.Meta & ~index.ToFlag();
if (newFlag == ApplyMeta) if (newFlag == Application.Meta)
return false; return false;
ApplyMeta = newFlag; Application.Meta = newFlag;
return true; return true;
} }
@ -128,103 +119,103 @@ public class DesignBase
} }
public bool DoApplyEquip(EquipSlot slot) public bool DoApplyEquip(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToFlag()); => Application.Equip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot) public bool DoApplyStain(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToStainFlag()); => Application.Equip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx) public bool DoApplyCustomize(CustomizeIndex idx)
=> ApplyCustomize.HasFlag(idx.ToFlag()); => Application.Customize.HasFlag(idx.ToFlag());
public bool DoApplyCrest(CrestFlag slot) public bool DoApplyCrest(CrestFlag slot)
=> ApplyCrest.HasFlag(slot); => Application.Crest.HasFlag(slot);
public bool DoApplyParameter(CustomizeParameterFlag flag) public bool DoApplyParameter(CustomizeParameterFlag flag)
=> ApplyParameters.HasFlag(flag); => Application.Parameters.HasFlag(flag);
public bool DoApplyBonusItem(BonusItemFlag slot)
=> Application.BonusItem.HasFlag(slot);
internal bool SetApplyEquip(EquipSlot slot, bool value) internal bool SetApplyEquip(EquipSlot slot, bool value)
{ {
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); var newValue = value ? Application.Equip | slot.ToFlag() : Application.Equip & ~slot.ToFlag();
if (newValue == ApplyEquip) if (newValue == Application.Equip)
return false; 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; return true;
} }
internal bool SetApplyStain(EquipSlot slot, bool value) internal bool SetApplyStain(EquipSlot slot, bool value)
{ {
var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); var newValue = value ? Application.Equip | slot.ToStainFlag() : Application.Equip & ~slot.ToStainFlag();
if (newValue == ApplyEquip) if (newValue == Application.Equip)
return false; return false;
ApplyEquip = newValue; Application.Equip = newValue;
return true; return true;
} }
internal bool SetApplyCustomize(CustomizeIndex idx, bool value) internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{ {
var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag(); var newValue = value ? Application.Customize | idx.ToFlag() : Application.Customize & ~idx.ToFlag();
if (newValue == _applyCustomize) if (newValue == Application.Customize)
return false; return false;
_applyCustomize = newValue; Application.Customize = newValue;
return true; return true;
} }
internal bool SetApplyCrest(CrestFlag slot, bool value) internal bool SetApplyCrest(CrestFlag slot, bool value)
{ {
var newValue = value ? ApplyCrest | slot : ApplyCrest & ~slot; var newValue = value ? Application.Crest | slot : Application.Crest & ~slot;
if (newValue == ApplyCrest) if (newValue == Application.Crest)
return false; return false;
ApplyCrest = newValue; Application.Crest = newValue;
return true; return true;
} }
internal bool SetApplyParameter(CustomizeParameterFlag flag, bool value) internal bool SetApplyParameter(CustomizeParameterFlag flag, bool value)
{ {
var newValue = value ? ApplyParameters | flag : ApplyParameters & ~flag; var newValue = value ? Application.Parameters | flag : Application.Parameters & ~flag;
if (newValue == ApplyParameters) if (newValue == Application.Parameters)
return false; return false;
ApplyParameters = newValue; Application.Parameters = newValue;
return true; return true;
} }
internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, public IEnumerable<string> FilteredItemNames
CustomizeParameterFlag parameterFlags) => _designData.FilteredItemNames(Application.Equip, Application.BonusItem);
=> new(this, equipFlags, customizeFlags, crestFlags, parameterFlags);
internal FlagRestrictionResetter TemporarilyRestrictApplication(ApplicationCollection restrictions)
=> new(this, restrictions);
internal readonly struct FlagRestrictionResetter : IDisposable internal readonly struct FlagRestrictionResetter : IDisposable
{ {
private readonly DesignBase _design; private readonly DesignBase _design;
private readonly EquipFlag _oldEquipFlags; private readonly ApplicationCollection _oldFlags;
private readonly CustomizeFlag _oldCustomizeFlags;
private readonly CrestFlag _oldCrestFlags;
private readonly CustomizeParameterFlag _oldParameterFlags;
public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, public FlagRestrictionResetter(DesignBase d, ApplicationCollection restrictions)
CustomizeParameterFlag parameterFlags)
{ {
_design = d; _design = d;
_oldEquipFlags = d.ApplyEquip; _oldFlags = d.Application;
_oldCustomizeFlags = d.ApplyCustomizeRaw; _design.Application = restrictions.Restrict(_oldFlags);
_oldCrestFlags = d.ApplyCrest;
_oldParameterFlags = d.ApplyParameters;
d.ApplyEquip &= equipFlags;
d.ApplyCustomize &= customizeFlags;
d.ApplyCrest &= crestFlags;
d.ApplyParameters &= parameterFlags;
} }
public void Dispose() public void Dispose()
{ => _design.Application = _oldFlags;
_design.ApplyEquip = _oldEquipFlags;
_design.ApplyCustomize = _oldCustomizeFlags;
_design.ApplyCrest = _oldCrestFlags;
_design.ApplyParameters = _oldParameterFlags;
}
} }
private CustomizeSet SetCustomizationSet(CustomizeService customize) private CustomizeSet SetCustomizationSet(CustomizeService customize)
@ -242,6 +233,7 @@ public class DesignBase
{ {
["FileVersion"] = FileVersion, ["FileVersion"] = FileVersion,
["Equipment"] = SerializeEquipment(), ["Equipment"] = SerializeEquipment(),
["Bonus"] = SerializeBonusItems(),
["Customize"] = SerializeCustomize(), ["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(), ["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(), ["Materials"] = SerializeMaterials(),
@ -257,15 +249,16 @@ public class DesignBase
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{ {
var item = _designData.Item(slot); var item = _designData.Item(slot);
var stain = _designData.Stain(slot); var stains = _designData.Stain(slot);
var crestSlot = slot.ToCrestFlag(); var crestSlot = slot.ToCrestFlag();
var crest = _designData.Crest(crestSlot); var crest = _designData.Crest(crestSlot);
ret[slot.ToString()] = Serialize(item.Id, stain, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot)); ret[slot.ToString()] = Serialize(item.Id, stains, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot));
} }
ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply"); ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply");
ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply"); ret["VieraEars"] = new QuadBool(_designData.AreEarsVisible(), DoApplyMeta(MetaIndex.EarState)).ToJObject("Show", "Apply");
ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).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 else
{ {
@ -274,16 +267,31 @@ public class DesignBase
return ret; return ret;
static JObject Serialize(CustomItemId id, StainId stain, bool crest, bool apply, bool applyStain, bool applyCrest) static JObject Serialize(CustomItemId id, StainIds stains, bool crest, bool apply, bool applyStain, bool applyCrest)
=> new() => stains.AddToObject(new JObject
{ {
["ItemId"] = id.Id, ["ItemId"] = id.Id,
["Stain"] = stain.Id,
["Crest"] = crest, ["Crest"] = crest,
["Apply"] = apply, ["Apply"] = apply,
["ApplyStain"] = applyStain, ["ApplyStain"] = applyStain,
["ApplyCrest"] = applyCrest, ["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;
} }
protected JObject SerializeCustomize() protected JObject SerializeCustomize()
@ -300,7 +308,7 @@ public class DesignBase
ret[idx.ToString()] = new JObject() ret[idx.ToString()] = new JObject()
{ {
["Value"] = customize[idx].Value, ["Value"] = customize[idx].Value,
["Apply"] = ApplyCustomizeRaw.HasFlag(idx.ToFlag()), ["Apply"] = Application.Customize.HasFlag(idx.ToFlag()),
}; };
} }
else else
@ -383,7 +391,7 @@ public class DesignBase
{ {
var k = uint.Parse(key.Name, NumberStyles.HexNumber); var k = uint.Parse(key.Name, NumberStyles.HexNumber);
var v = value.ToObject<MaterialValueDesign>(); var v = value.ToObject<MaterialValueDesign>();
if (!MaterialValueIndex.FromKey(k, out var idx)) if (!MaterialValueIndex.FromKey(k, out _))
{ {
Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.", Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.",
NotificationType.Warning); NotificationType.Warning);
@ -423,19 +431,43 @@ public class DesignBase
LoadEquip(items, json["Equipment"], ret, "Temporary Design", true); LoadEquip(items, json["Equipment"], ret, "Temporary Design", true);
LoadParameters(json["Parameters"], ret, "Temporary Design"); LoadParameters(json["Parameters"], ret, "Temporary Design");
LoadMaterials(json["Materials"], ret, "Temporary Design"); LoadMaterials(json["Materials"], ret, "Temporary Design");
LoadBonus(items, ret, json["Bonus"]);
return ret; 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) protected static void LoadParameters(JToken? parameters, DesignBase design, string name)
{ {
if (parameters == null) if (parameters == null)
{ {
design.ApplyParameters = 0; design.Application.Parameters = 0;
design.GetDesignDataRef().Parameters = default; design.GetDesignDataRef().Parameters = default;
return; return;
} }
foreach (var flag in CustomizeParameterExtensions.ValueFlags) foreach (var flag in CustomizeParameterExtensions.ValueFlags)
{ {
if (!TryGetToken(flag, out var token)) if (!TryGetToken(flag, out var token))
@ -491,7 +523,7 @@ public class DesignBase
return true; return true;
} }
design.ApplyParameters &= ~flag; design.Application.Parameters &= ~flag;
design.GetDesignDataRef().Parameters[flag] = CustomizeParameterValue.Zero; design.GetDesignDataRef().Parameters[flag] = CustomizeParameterValue.Zero;
return false; return false;
} }
@ -522,32 +554,15 @@ public class DesignBase
return; return;
} }
static (CustomItemId, StainId, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
var id = item?["ItemId"]?.ToObject<ulong>() ?? ItemManager.NothingId(slot).Id;
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
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, stain, crest, apply, applyStain, applyCrest);
}
void PrintWarning(string msg)
{
if (msg.Length > 0 && name != "Temporary Design")
Glamourer.Messager.NotificationMessage($"{msg} ({name})", NotificationType.Warning);
}
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)
{ {
var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]); var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]);
PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown)); PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown));
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown)); PrintWarning(items.ValidateStain(stains, out stains, allowUnknown));
var crestSlot = slot.ToCrestFlag(); var crestSlot = slot.ToCrestFlag();
design._designData.SetItem(slot, item); design._designData.SetItem(slot, item);
design._designData.SetStain(slot, stain); design._designData.SetStain(slot, stains);
design._designData.SetCrest(crestSlot, crest); design._designData.SetCrest(crestSlot, crest);
design.SetApplyEquip(slot, apply); design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain); design.SetApplyStain(slot, applyStain);
@ -555,21 +570,21 @@ public class DesignBase
} }
{ {
var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand)) if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.ItemId; id = items.DefaultSword.ItemId;
var (idOff, stainOff, crestOff, applyOff, applyStainOff, applyCrestOff) = var (idOff, stainsOff, crestOff, applyOff, applyStainOff, applyCrestOff) =
ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand)) if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield); id = ItemManager.NothingId(FullEquipType.Shield);
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown)); PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown));
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown)); PrintWarning(items.ValidateStain(stains, out stains, allowUnknown));
PrintWarning(items.ValidateStain(stainOff, out stainOff, allowUnknown)); PrintWarning(items.ValidateStain(stainsOff, out stainsOff, allowUnknown));
design._designData.SetItem(EquipSlot.MainHand, main); design._designData.SetItem(EquipSlot.MainHand, main);
design._designData.SetItem(EquipSlot.OffHand, off); design._designData.SetItem(EquipSlot.OffHand, off);
design._designData.SetStain(EquipSlot.MainHand, stain); design._designData.SetStain(EquipSlot.MainHand, stains);
design._designData.SetStain(EquipSlot.OffHand, stainOff); design._designData.SetStain(EquipSlot.OffHand, stainsOff);
design._designData.SetCrest(CrestFlag.MainHand, crest); design._designData.SetCrest(CrestFlag.MainHand, crest);
design._designData.SetCrest(CrestFlag.OffHand, crestOff); design._designData.SetCrest(CrestFlag.OffHand, crestOff);
design.SetApplyEquip(EquipSlot.MainHand, apply); design.SetApplyEquip(EquipSlot.MainHand, apply);
@ -590,6 +605,28 @@ public class DesignBase
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled); design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled);
design._designData.SetVisor(metaValue.ForcedValue); 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);
}
static (CustomItemId, StainIds, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
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);
}
} }
protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman, protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
@ -670,11 +707,12 @@ public class DesignBase
{ {
_designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags, _designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags,
out var writeProtected, out var applyMeta); out var writeProtected, out var applyMeta);
ApplyEquip = equipFlags; Application.Equip = equipFlags;
ApplyCustomize = customizeFlags; ApplyCustomize = customizeFlags;
ApplyParameters = 0; Application.Parameters = 0;
ApplyCrest = 0; Application.Crest = 0;
ApplyMeta = applyMeta; Application.Meta = applyMeta;
Application.BonusItem = 0;
SetWriteProtected(writeProtected); SetWriteProtected(writeProtected);
CustomizeSet = SetCustomizationSet(customize); CustomizeSet = SetCustomizationSet(customize);
} }

View file

@ -1,6 +1,7 @@
using Glamourer.Services; using Glamourer.Api.Enums;
using Glamourer.State; using Glamourer.Services;
using OtterGui; using OtterGui;
using OtterGui.Extensions;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -96,8 +97,8 @@ public class DesignBase64Migration
fixed (byte* ptr = bytes) fixed (byte* ptr = bytes)
{ {
var cur = (CharacterWeapon*)(ptr + 30); var cur = (LegacyCharacterWeapon*)(ptr + 30);
var eq = (CharacterArmor*)(cur + 2); var eq = (LegacyCharacterArmor*)(cur + 2);
if (!humans.IsHuman(data.ModelId)) if (!humans.IsHuman(data.ModelId))
{ {
@ -163,7 +164,8 @@ public class DesignBase64Migration
} }
} }
public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, MetaFlag meta, 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]; var data = stackalloc byte[Base64SizeV4];
data[0] = 5; data[0] = 5;
@ -186,10 +188,12 @@ public class DesignBase64Migration
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0) | (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0)); | (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
save.Customize.Write(data + 4); save.Customize.Write(data + 4);
((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand)); ((LegacyCharacterWeapon*)(data + 30))[0] =
((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand)); 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) 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. *(ushort*)(data + 84) = 1; // IsSet.
*(float*)(data + 86) = 1f; *(float*)(data + 86) = 1f;
data[90] = (byte)((save.IsHatVisible() ? 0x00 : 0x01) data[90] = (byte)((save.IsHatVisible() ? 0x00 : 0x01)

View file

@ -1,13 +1,14 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Services; using Glamourer.Services;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Extensions;
namespace Glamourer.Designs; namespace Glamourer.Designs;
@ -270,7 +271,7 @@ public class DesignColors : ISavable, IReadOnlyDictionary<string, uint>
public static uint AutoColor(DesignBase design) public static uint AutoColor(DesignBase design)
{ {
var customize = design.ApplyCustomizeExcludingBodyType == 0; var customize = design.ApplyCustomizeExcludingBodyType == 0;
var equip = design.ApplyEquip == 0; var equip = design.Application.Equip == 0;
return (customize, equip) switch return (customize, equip) switch
{ {
(true, true) => ColorId.StateDesign.Value(), (true, true) => ColorId.StateDesign.Value(),

View file

@ -7,12 +7,13 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
public class DesignConverter( public class DesignConverter(
SaveService saveService,
ItemManager _items, ItemManager _items,
DesignManager _designs, DesignManager _designs,
CustomizeService _customize, CustomizeService _customize,
@ -34,10 +35,10 @@ public class DesignConverter(
} }
public string ShareBase64(Design design) public string ShareBase64(Design design)
=> ShareBase64(ShareJObject(design)); => ToBase64(ShareJObject(design));
public string ShareBase64(DesignBase design) public string ShareBase64(DesignBase design)
=> ShareBase64(ShareJObject(design)); => ToBase64(ShareJObject(design));
public string ShareBase64(ActorState state, in ApplicationRules rules) public string ShareBase64(ActorState state, in ApplicationRules rules)
=> ShareBase64(state.ModelData, state.Materials, rules); => ShareBase64(state.ModelData, state.Materials, rules);
@ -45,7 +46,7 @@ public class DesignConverter(
public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules) public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
{ {
var design = Convert(data, materials, rules); var design = Convert(data, materials, rules);
return ShareBase64(ShareJObject(design)); return ToBase64(ShareJObject(design));
} }
public DesignBase Convert(ActorState state, in ApplicationRules rules) public DesignBase Convert(ActorState state, in ApplicationRules rules)
@ -56,10 +57,37 @@ public class DesignConverter(
var design = _designs.CreateTemporary(); var design = _designs.CreateTemporary();
rules.Apply(design); rules.Apply(design);
design.SetDesignData(_customize, data); design.SetDesignData(_customize, data);
ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip); if (rules.Materials)
ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip);
return design; 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) public DesignBase? FromBase64(string base64, bool customize, bool equip, out byte version)
{ {
DesignBase ret; DesignBase ret;
@ -73,7 +101,7 @@ public class DesignConverter(
case (byte)'{': case (byte)'{':
var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes)); var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes));
ret = jObj1["Identifier"] != null ret = jObj1["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj1) ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj1)
: DesignBase.LoadDesignBase(_customize, _items, jObj1); : DesignBase.LoadDesignBase(_customize, _items, jObj1);
break; break;
case 1: case 1:
@ -88,7 +116,7 @@ public class DesignConverter(
var jObj2 = JObject.Parse(decompressed); var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 3); Debug.Assert(version == 3);
ret = jObj2["Identifier"] != null ret = jObj2["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2); : DesignBase.LoadDesignBase(_customize, _items, jObj2);
break; break;
} }
@ -99,7 +127,7 @@ public class DesignConverter(
var jObj2 = JObject.Parse(decompressed); var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 5); Debug.Assert(version == 5);
ret = jObj2["Identifier"] != null ret = jObj2["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2); : DesignBase.LoadDesignBase(_customize, _items, jObj2);
break; break;
} }
@ -109,7 +137,7 @@ public class DesignConverter(
var jObj2 = JObject.Parse(decompressed); var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 6); Debug.Assert(version == 6);
ret = jObj2["Identifier"] != null ret = jObj2["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2); : DesignBase.LoadDesignBase(_customize, _items, jObj2);
break; break;
} }
@ -123,28 +151,23 @@ public class DesignConverter(
return null; return null;
} }
ret.SetApplyMeta(MetaIndex.Wetness, customize);
if (!customize) if (!customize)
ret.ApplyCustomize = 0; ret.Application.RemoveCustomize();
if (!equip) if (!equip)
{ ret.Application.RemoveEquip();
ret.ApplyEquip = 0;
ret.ApplyCrest = 0;
ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState);
}
return ret; return ret;
} }
private static string ShareBase64(JToken jObject) public static string ToBase64(JToken jObject)
{ {
var json = jObject.ToString(Formatting.None); var json = jObject.ToString(Formatting.None);
var compressed = json.Compress(Version); var compressed = json.Compress(Version);
return System.Convert.ToBase64String(compressed); return System.Convert.ToBase64String(compressed);
} }
public IEnumerable<(EquipSlot Slot, EquipItem Item, StainId Stain)> FromDrawData(IReadOnlyList<CharacterArmor> armors, public IEnumerable<(EquipSlot Slot, EquipItem Item, StainIds Stains)> FromDrawData(IReadOnlyList<CharacterArmor> armors,
CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings) CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings)
{ {
if (armors.Count != 10) if (armors.Count != 10)
@ -162,7 +185,7 @@ public class DesignConverter(
item = ItemManager.NothingItem(slot); item = ItemManager.NothingItem(slot);
} }
yield return (slot, item, armor.Stain); yield return (slot, item, armor.Stains);
} }
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant); var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant);
@ -172,7 +195,7 @@ public class DesignConverter(
mh = _items.DefaultSword; mh = _items.DefaultSword;
} }
yield return (EquipSlot.MainHand, mh, mainhand.Stain); yield return (EquipSlot.MainHand, mh, mainhand.Stains);
var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type); var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type);
if (!skipWarnings && !oh.Valid) if (!skipWarnings && !oh.Valid)
@ -183,29 +206,47 @@ public class DesignConverter(
oh = ItemManager.NothingItem(FullEquipType.Shield); oh = ItemManager.NothingItem(FullEquipType.Shield);
} }
yield return (EquipSlot.OffHand, oh, offhand.Stain); yield return (EquipSlot.OffHand, oh, offhand.Stains);
} }
private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials, private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials,
EquipFlag equipFlags = EquipFlagExtensions.All) EquipFlag equipFlags = EquipFlagExtensions.All, BonusItemFlag bonusFlags = BonusExtensions.All)
{ {
foreach (var (key, value) in materials.Values) foreach (var (key, value) in materials.Values)
{ {
var idx = MaterialValueIndex.FromKey(key); var idx = MaterialValueIndex.FromKey(key);
if (idx.RowIndex >= MtrlFile.ColorTable.NumRows) if (idx.RowIndex >= ColorTable.NumRows)
continue; continue;
if (idx.MaterialIndex >= MaterialService.MaterialsPerModel) if (idx.MaterialIndex >= MaterialService.MaterialsPerModel)
continue; continue;
var slot = idx.DrawObject switch switch (idx.DrawObject)
{ {
MaterialValueIndex.DrawObjectType.Human => idx.SlotIndex < 10 ? ((uint)idx.SlotIndex).ToEquipSlot() : EquipSlot.Unknown, case MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0:
MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0 => EquipSlot.MainHand, if ((equipFlags & (EquipFlag.Mainhand | EquipFlag.MainhandStain)) == 0)
MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0 => EquipSlot.OffHand, continue;
_ => EquipSlot.Unknown,
}; break;
if (slot is EquipSlot.Unknown || (slot.ToBothFlags() & equipFlags) == 0) case MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0:
continue; 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()); manager.AddOrUpdateValue(idx, value.Convert());
} }

View file

@ -9,27 +9,35 @@ namespace Glamourer.Designs;
public unsafe struct DesignData public unsafe struct DesignData
{ {
private string _nameHead = string.Empty; public const int NumEquipment = 10;
private string _nameBody = string.Empty; public const int EquipmentByteSize = NumEquipment * CharacterArmor.Size;
private string _nameHands = string.Empty; public const int NumBonusItems = 1;
private string _nameLegs = string.Empty; public const int NumWeapons = 2;
private string _nameFeet = string.Empty;
private string _nameEars = string.Empty; private string _nameHead = string.Empty;
private string _nameNeck = string.Empty; private string _nameBody = string.Empty;
private string _nameWrists = string.Empty; private string _nameHands = string.Empty;
private string _nameRFinger = string.Empty; private string _nameLegs = string.Empty;
private string _nameLFinger = string.Empty; private string _nameFeet = string.Empty;
private string _nameMainhand = string.Empty; private string _nameEars = string.Empty;
private string _nameOffhand = string.Empty; private string _nameNeck = string.Empty;
private fixed uint _itemIds[12]; private string _nameWrists = string.Empty;
private fixed ushort _iconIds[12]; private string _nameRFinger = string.Empty;
private fixed byte _equipmentBytes[48]; 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 CustomizeParameterData Parameters;
public CustomizeArray Customize = CustomizeArray.Default; public CustomizeArray Customize = CustomizeArray.Default;
public uint ModelId; public uint ModelId;
public CrestFlag CrestVisibility; public CrestFlag CrestVisibility;
private SecondaryId _secondaryMainhand;
private SecondaryId _secondaryOffhand;
private FullEquipType _typeMainhand; private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand; private FullEquipType _typeOffhand;
private byte _states; private byte _states;
@ -38,29 +46,76 @@ public unsafe struct DesignData
public DesignData() public DesignData()
{ } { }
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public readonly bool ContainsName(LowerString name) public readonly bool ContainsName(LowerString name)
=> name.IsContained(_nameHead) => ItemNames.Any(name.IsContained);
|| name.IsContained(_nameBody)
|| name.IsContained(_nameHands)
|| name.IsContained(_nameLegs)
|| name.IsContained(_nameFeet)
|| name.IsContained(_nameEars)
|| name.IsContained(_nameNeck)
|| name.IsContained(_nameWrists)
|| name.IsContained(_nameRFinger)
|| name.IsContained(_nameLFinger)
|| name.IsContained(_nameMainhand)
|| name.IsContained(_nameOffhand);
public readonly StainId Stain(EquipSlot slot) public readonly StainIds Stain(EquipSlot slot)
{ {
var index = slot.ToIndex(); 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 readonly bool Crest(CrestFlag slot) public readonly bool Crest(CrestFlag slot)
=> CrestVisibility.HasFlag(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 public readonly FullEquipType MainhandType
=> _typeMainhand; => _typeMainhand;
@ -69,22 +124,36 @@ public unsafe struct DesignData
=> _typeOffhand; => _typeOffhand;
public readonly EquipItem Item(EquipSlot slot) 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 // @formatter:off
0 => EquipItem.FromIds((ItemId)_itemIds[ 0], (IconId)_iconIds[ 0], (PrimaryId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ), BonusItemFlag.Glasses => EquipItem.FromBonusIds(_bonusIds[0], _iconIds[12], _bonusModelIds[0], _bonusVariants[0], BonusItemFlag.Glasses, _nameGlasses),
1 => EquipItem.FromIds((ItemId)_itemIds[ 1], (IconId)_iconIds[ 1], (PrimaryId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ), _ => EquipItem.BonusItemNothing(slot),
2 => EquipItem.FromIds((ItemId)_itemIds[ 2], (IconId)_iconIds[ 2], (PrimaryId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[10], FullEquipType.Hands, name: _nameHands ),
3 => EquipItem.FromIds((ItemId)_itemIds[ 3], (IconId)_iconIds[ 3], (PrimaryId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ),
4 => EquipItem.FromIds((ItemId)_itemIds[ 4], (IconId)_iconIds[ 4], (PrimaryId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ),
5 => EquipItem.FromIds((ItemId)_itemIds[ 5], (IconId)_iconIds[ 5], (PrimaryId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[22], FullEquipType.Ears, name: _nameEars ),
6 => EquipItem.FromIds((ItemId)_itemIds[ 6], (IconId)_iconIds[ 6], (PrimaryId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ),
7 => EquipItem.FromIds((ItemId)_itemIds[ 7], (IconId)_iconIds[ 7], (PrimaryId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ),
8 => EquipItem.FromIds((ItemId)_itemIds[ 8], (IconId)_iconIds[ 8], (PrimaryId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ),
9 => EquipItem.FromIds((ItemId)_itemIds[ 9], (IconId)_iconIds[ 9], (PrimaryId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ),
10 => EquipItem.FromIds((ItemId)_itemIds[10], (IconId)_iconIds[10], (PrimaryId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, (Variant)_equipmentBytes[42], _typeMainhand, name: _nameMainhand),
11 => EquipItem.FromIds((ItemId)_itemIds[11], (IconId)_iconIds[11], (PrimaryId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, (Variant)_equipmentBytes[46], _typeOffhand, name: _nameOffhand ),
_ => new EquipItem(),
// @formatter:on // @formatter:on
}; };
@ -113,22 +182,22 @@ public unsafe struct DesignData
{ {
fixed (byte* ptr = _equipmentBytes) fixed (byte* ptr = _equipmentBytes)
{ {
var armorPtr = (CharacterArmor*)ptr; var weaponPtr = (CharacterWeapon*)(ptr + EquipmentByteSize);
return slot is EquipSlot.MainHand ? armorPtr[10].ToWeapon(_secondaryMainhand) : armorPtr[11].ToWeapon(_secondaryOffhand); return weaponPtr[slot is EquipSlot.MainHand ? 0 : 1];
} }
} }
public bool SetItem(EquipSlot slot, EquipItem item) public bool SetItem(EquipSlot slot, EquipItem item)
{ {
var index = slot.ToIndex(); var index = slot.ToIndex();
if (index > 11) if (index > NumEquipment + NumWeapons)
return false; return false;
_itemIds[index] = item.ItemId.Id; _itemIds[index] = item.ItemId.Id;
_iconIds[index] = item.IconId.Id; _iconIds[index] = item.IconId.Id;
_equipmentBytes[4 * index + 0] = (byte)item.PrimaryId.Id; _equipmentBytes[CharacterArmor.Size * index + 0] = (byte)item.PrimaryId.Id;
_equipmentBytes[4 * index + 1] = (byte)(item.PrimaryId.Id >> 8); _equipmentBytes[CharacterArmor.Size * index + 1] = (byte)(item.PrimaryId.Id >> 8);
_equipmentBytes[4 * index + 2] = item.Variant.Id; _equipmentBytes[CharacterArmor.Size * index + 2] = item.Variant.Id;
switch (index) switch (index)
{ {
// @formatter:off // @formatter:off
@ -144,36 +213,61 @@ public unsafe struct DesignData
case 9: _nameLFinger = item.Name; return true; case 9: _nameLFinger = item.Name; return true;
// @formatter:on // @formatter:on
case 10: case 10:
_nameMainhand = item.Name; _nameMainhand = item.Name;
_secondaryMainhand = item.SecondaryId; _equipmentBytes[EquipmentByteSize + 2] = (byte)item.SecondaryId.Id;
_typeMainhand = item.Type; _equipmentBytes[EquipmentByteSize + 3] = (byte)(item.SecondaryId.Id >> 8);
_equipmentBytes[EquipmentByteSize + 4] = item.Variant.Id;
_typeMainhand = item.Type;
return true; return true;
case 11: case 11:
_nameOffhand = item.Name; _nameOffhand = item.Name;
_secondaryOffhand = item.SecondaryId; _equipmentBytes[EquipmentByteSize + 10] = (byte)item.SecondaryId.Id;
_typeOffhand = item.Type; _equipmentBytes[EquipmentByteSize + 11] = (byte)(item.SecondaryId.Id >> 8);
_equipmentBytes[EquipmentByteSize + 12] = item.Variant.Id;
_typeOffhand = item.Type;
return true; return true;
} }
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 => slot.ToIndex() switch
{ {
0 => SetIfDifferent(ref _equipmentBytes[3], stain.Id), // @formatter:off
1 => SetIfDifferent(ref _equipmentBytes[7], stain.Id), 0 => SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 4], stains.Stain2.Id),
2 => SetIfDifferent(ref _equipmentBytes[11], stain.Id), 1 => SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 4], stains.Stain2.Id),
3 => SetIfDifferent(ref _equipmentBytes[15], stain.Id), 2 => SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 4], stains.Stain2.Id),
4 => SetIfDifferent(ref _equipmentBytes[19], stain.Id), 3 => SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 4], stains.Stain2.Id),
5 => SetIfDifferent(ref _equipmentBytes[23], stain.Id), 4 => SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 4], stains.Stain2.Id),
6 => SetIfDifferent(ref _equipmentBytes[27], stain.Id), 5 => SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 4], stains.Stain2.Id),
7 => SetIfDifferent(ref _equipmentBytes[31], stain.Id), 6 => SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 4], stains.Stain2.Id),
8 => SetIfDifferent(ref _equipmentBytes[35], stain.Id), 7 => SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 4], stains.Stain2.Id),
9 => SetIfDifferent(ref _equipmentBytes[39], stain.Id), 8 => SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 4], stains.Stain2.Id),
10 => SetIfDifferent(ref _equipmentBytes[43], stain.Id), 9 => SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 4], stains.Stain2.Id),
11 => SetIfDifferent(ref _equipmentBytes[47], stain.Id), 10 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 6], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 7], stains.Stain2.Id),
_ => false, 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) public bool SetCrest(CrestFlag slot, bool visible)
@ -193,6 +287,7 @@ public unsafe struct DesignData
MetaIndex.HatState => IsHatVisible(), MetaIndex.HatState => IsHatVisible(),
MetaIndex.VisorState => IsVisorToggled(), MetaIndex.VisorState => IsVisorToggled(),
MetaIndex.WeaponState => IsWeaponVisible(), MetaIndex.WeaponState => IsWeaponVisible(),
MetaIndex.EarState => AreEarsVisible(),
_ => false, _ => false,
}; };
@ -203,6 +298,7 @@ public unsafe struct DesignData
MetaIndex.HatState => SetHatVisible(value), MetaIndex.HatState => SetHatVisible(value),
MetaIndex.VisorState => SetVisor(value), MetaIndex.VisorState => SetVisor(value),
MetaIndex.WeaponState => SetWeaponVisible(value), MetaIndex.WeaponState => SetWeaponVisible(value),
MetaIndex.EarState => SetEarsVisible(value),
_ => false, _ => false,
}; };
@ -246,6 +342,9 @@ public unsafe struct DesignData
public readonly bool IsWeaponVisible() public readonly bool IsWeaponVisible()
=> (_states & 0x08) == 0x08; => (_states & 0x08) == 0x08;
public readonly bool AreEarsVisible()
=> (_states & 0x10) == 0x00;
public bool SetWeaponVisible(bool value) public bool SetWeaponVisible(bool value)
{ {
if (value == IsWeaponVisible()) if (value == IsWeaponVisible())
@ -255,21 +354,37 @@ public unsafe struct DesignData
return true; 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) public void SetDefaultEquipment(ItemManager items)
{ {
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)
{ {
SetItem(slot, ItemManager.NothingItem(slot)); SetItem(slot, ItemManager.NothingItem(slot));
SetStain(slot, 0); SetStain(slot, StainIds.None);
SetCrest(slot.ToCrestFlag(), false); SetCrest(slot.ToCrestFlag(), false);
} }
SetItem(EquipSlot.MainHand, items.DefaultSword); SetItem(EquipSlot.MainHand, items.DefaultSword);
SetStain(EquipSlot.MainHand, 0); SetStain(EquipSlot.MainHand, StainIds.None);
SetCrest(CrestFlag.MainHand, false); SetCrest(CrestFlag.MainHand, false);
SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield)); SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield));
SetStain(EquipSlot.OffHand, 0); SetStain(EquipSlot.OffHand, StainIds.None);
SetCrest(CrestFlag.OffHand, false); SetCrest(CrestFlag.OffHand, false);
SetDefaultBonusItems();
}
public void SetDefaultBonusItems()
{
foreach (var slot in BonusExtensions.AllFlags)
SetBonusItem(slot, EquipItem.BonusItemNothing(slot));
} }
@ -285,13 +400,14 @@ public unsafe struct DesignData
SetHatVisible(true); SetHatVisible(true);
SetWeaponVisible(true); SetWeaponVisible(true);
SetEarsVisible(true);
SetVisor(false); SetVisor(false);
fixed (uint* ptr = _itemIds) fixed (uint* ptr = _itemIds)
{ {
MemoryUtility.MemSet(ptr, 0, 10 * 4); MemoryUtility.MemSet(ptr, 0, 10 * 4);
} }
fixed (ushort* ptr = _iconIds) fixed (uint* ptr = _iconIds)
{ {
MemoryUtility.MemSet(ptr, 0, 10 * 2); MemoryUtility.MemSet(ptr, 0, 10 * 2);
} }
@ -306,6 +422,7 @@ public unsafe struct DesignData
_nameWrists = string.Empty; _nameWrists = string.Empty;
_nameRFinger = string.Empty; _nameRFinger = string.Empty;
_nameLFinger = string.Empty; _nameLFinger = string.Empty;
_nameGlasses = string.Empty;
return true; return true;
} }
@ -322,7 +439,7 @@ public unsafe struct DesignData
public readonly byte[] GetEquipmentBytes() public readonly byte[] GetEquipmentBytes()
{ {
var ret = new byte[40]; var ret = new byte[80];
fixed (byte* retPtr = ret, inPtr = _equipmentBytes) fixed (byte* retPtr = ret, inPtr = _equipmentBytes)
{ {
MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length); MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length);
@ -343,8 +460,8 @@ public unsafe struct DesignData
{ {
fixed (byte* dataPtr = _equipmentBytes) fixed (byte* dataPtr = _equipmentBytes)
{ {
var data = new Span<byte>(dataPtr, 40); var data = new Span<byte>(dataPtr, 80);
return Convert.TryFromBase64String(base64, data, out var written) && written == 40; return Convert.TryFromBase64String(base64, data, out var written) && written == 80;
} }
} }

View file

@ -1,9 +1,9 @@
using Glamourer.Designs.History;
using Glamourer.Designs.Links; using Glamourer.Designs.Links;
using Glamourer.Events; using Glamourer.Events;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.State;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -73,7 +73,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
SaveService.QueueSave(design); SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); DesignChanged.Invoke(DesignChanged.Type.Customize, design, new CustomizeTransaction(idx, oldValue, value));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -89,7 +89,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}."); Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}.");
SaveService.QueueSave(design); SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, (oldCustomize, applied, changed)); DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, new EntireCustomizeTransaction(changed, oldCustomize, newCustomize));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -104,7 +104,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}."); Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}.");
SaveService.QueueSave(design); SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Parameter, design, (old, @new, flag)); DesignChanged.Invoke(DesignChanged.Type.Parameter, design, new ParameterTransaction(flag, old, @new));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -123,11 +123,14 @@ public class DesignEditor(
if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets)) if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets))
return; return;
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug( Glamourer.Log.Debug(
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId})."); $"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, (currentMain, currentOff, item, newOff, newGauntlets)); DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
new WeaponTransaction(currentMain, currentOff, currentGauntlets, item, newOff ?? currentOff,
newGauntlets ?? currentGauntlets));
return; return;
} }
case EquipSlot.OffHand: case EquipSlot.OffHand:
@ -140,11 +143,13 @@ public class DesignEditor(
if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item)) if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item))
return; return;
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug( Glamourer.Log.Debug(
$"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId})."); $"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, (currentMain, currentOff, currentMain, item)); DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
new WeaponTransaction(currentMain, currentOff, currentGauntlets, currentMain, item, currentGauntlets));
return; return;
} }
default: default:
@ -160,36 +165,53 @@ public class DesignEditor(
Glamourer.Log.Debug( Glamourer.Log.Debug(
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId})."); $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}).");
SaveService.QueueSave(design); SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); DesignChanged.Invoke(DesignChanged.Type.Equip, design, new EquipTransaction(slot, old, item));
return; return;
} }
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings _ = default) public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default)
{ {
var design = (Design)data; var design = (Design)data;
if (Items.ValidateStain(stain, out var _, false).Length > 0) if (item.Type.ToBonus() != slot)
return; return;
var oldStain = design.DesignData.Stain(slot); var oldItem = design.DesignData.BonusItem(slot);
if (!design.GetDesignDataRef().SetStain(slot, stain)) if (!design.GetDesignDataRef().SetBonusItem(slot, item))
return; return;
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}."); Glamourer.Log.Debug($"Set {slot} bonus item to {item}.");
DesignChanged.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, new BonusItemTransaction(slot, oldItem, item));
} }
/// <inheritdoc/> /// <inheritdoc/>
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings _ = default) 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) if (item.HasValue)
ChangeItem(data, slot, item.Value, _); ChangeItem(data, slot, item.Value, _);
if (stain.HasValue) if (stains.HasValue)
ChangeStain(data, slot, stain.Value, _); ChangeStains(data, slot, stains.Value, _);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -203,7 +225,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}."); Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}.");
DesignChanged.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot)); DesignChanged.Invoke(DesignChanged.Type.Crest, design, new CrestTransaction(slot, oldCrest, crest));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -216,7 +238,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); Glamourer.Log.Debug($"Set value of {metaIndex} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value)); DesignChanged.Invoke(DesignChanged.Type.Other, design, new MetaTransaction(metaIndex, !value, value));
} }
public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert) public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert)
@ -229,7 +251,7 @@ public class DesignEditor(
Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}"); Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}");
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, index); DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert));
} }
public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row) public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row)
@ -264,7 +286,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.DelaySave(design); SaveService.DelaySave(design);
DesignChanged.Invoke(DesignChanged.Type.Material, design, (oldValue, row, index)); DesignChanged.Invoke(DesignChanged.Type.Material, design, new MaterialTransaction(index, oldValue.Value, row));
} }
public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value) public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value)
@ -277,13 +299,13 @@ public class DesignEditor(
Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}."); Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}.");
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, index); DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, new ApplicationTransaction(index, !value, value));
} }
/// <inheritdoc/> /// <inheritdoc/>
public void ApplyDesign(object data, MergedDesign other, ApplySettings _ = default) public void ApplyDesign(object data, MergedDesign other, ApplySettings settings = default)
=> ApplyDesign(data, other.Design); => ApplyDesign(data, other.Design, settings);
/// <inheritdoc/> /// <inheritdoc/>
public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default) public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default)
@ -308,6 +330,12 @@ public class DesignEditor(
_forceFullItemOff = false; _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)) foreach (var slot in Enum.GetValues<CrestFlag>().Where(other.DoApplyCrest))
ChangeCrest(design, slot, other.DesignData.Crest(slot)); ChangeCrest(design, slot, other.DesignData.Crest(slot));
@ -338,7 +366,7 @@ public class DesignEditor(
newOff = o; newOff = o;
} }
else if (!_forceFullItemOff && Config.ChangeEntireItem) else if (!_forceFullItemOff && Config.ChangeEntireItem && newMain.Type is not FullEquipType.Sword) // Skip applying shields.
{ {
var defaultOffhand = Items.GetDefaultOffhand(newMain); var defaultOffhand = Items.GetDefaultOffhand(newMain);
if (Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o)) if (Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o))

View file

@ -1,4 +1,5 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Glamourer.Designs.History;
using Glamourer.Events; using Glamourer.Events;
using Glamourer.Services; using Glamourer.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -40,11 +41,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct CreationDate : ISortMode<Design> public struct CreationDate : ISortMode<Design>
{ {
public string Name public ReadOnlySpan<byte> Name
=> "Creation Date (Older First)"; => "Creation Date (Older First)"u8;
public string Description public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."; => "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."u8;
public IEnumerable<IPath> GetChildren(Folder f) public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate)); => f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate));
@ -52,11 +53,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct UpdateDate : ISortMode<Design> public struct UpdateDate : ISortMode<Design>
{ {
public string Name public ReadOnlySpan<byte> Name
=> "Update Date (Older First)"; => "Update Date (Older First)"u8;
public string Description public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."; => "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."u8;
public IEnumerable<IPath> GetChildren(Folder f) public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit)); => f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit));
@ -64,11 +65,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct InverseCreationDate : ISortMode<Design> public struct InverseCreationDate : ISortMode<Design>
{ {
public string Name public ReadOnlySpan<byte> Name
=> "Creation Date (Newer First)"; => "Creation Date (Newer First)"u8;
public string Description public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."; => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."u8;
public IEnumerable<IPath> GetChildren(Folder f) public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate)); => f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate));
@ -76,11 +77,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct InverseUpdateDate : ISortMode<Design> public struct InverseUpdateDate : ISortMode<Design>
{ {
public string Name public ReadOnlySpan<byte> Name
=> "Update Date (Newer First)"; => "Update Date (Newer First)"u8;
public string Description public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."; => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."u8;
public IEnumerable<IPath> GetChildren(Folder f) public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit)); => f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit));
@ -92,13 +93,13 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
_saveService.QueueSave(this); _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) switch (type)
{ {
case DesignChanged.Type.Created: case DesignChanged.Type.Created:
var parent = Root; var parent = Root;
if (data is string path) if ((data as CreationTransaction?)?.Path is { } path)
try try
{ {
parent = FindOrCreateAllFolders(path); parent = FindOrCreateAllFolders(path);
@ -113,14 +114,14 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
return; return;
case DesignChanged.Type.Deleted: case DesignChanged.Type.Deleted:
if (FindLeaf(design, out var leaf1)) if (TryGetValue(design, out var leaf1))
Delete(leaf1); Delete(leaf1);
return; return;
case DesignChanged.Type.ReloadedAll: case DesignChanged.Type.ReloadedAll:
Reload(); Reload();
return; return;
case DesignChanged.Type.Renamed when data is string oldName: case DesignChanged.Type.Renamed when (data as RenameTransaction?)?.Old is { } oldName:
if (!FindLeaf(design, out var leaf2)) if (!TryGetValue(design, out var leaf2))
return; return;
var old = oldName.FixName(); var old = oldName.FixName();
@ -149,15 +150,6 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
? (string.Empty, false) ? (string.Empty, false)
: (DesignToIdentifier(design), true); : (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) internal static void MigrateOldPaths(SaveService saveService, Dictionary<string, string> oldPaths)
{ {
if (oldPaths.Count == 0) if (oldPaths.Count == 0)

View file

@ -1,15 +1,18 @@
using Dalamud.Utility; using Dalamud.Utility;
using Glamourer.Designs.History;
using Glamourer.Designs.Links; using Glamourer.Designs.Links;
using Glamourer.Events; using Glamourer.Events;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra; using Glamourer.Interop.Penumbra;
using Glamourer.Services; using Glamourer.Services;
using OtterGui.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
namespace Glamourer.Designs; namespace Glamourer.Designs;
public sealed class DesignManager : DesignEditor public sealed class DesignManager : DesignEditor
@ -52,7 +55,7 @@ public sealed class DesignManager : DesignEditor
{ {
var text = File.ReadAllText(f.FullName); var text = File.ReadAllText(f.FullName);
var data = JObject.Parse(text); var data = JObject.Parse(text);
var design = Design.LoadDesign(Customizations, Items, linkLoader, data); var design = Design.LoadDesign(SaveService, Customizations, Items, linkLoader, data);
designs.Value!.Add((design, f.FullName)); designs.Value!.Add((design, f.FullName));
} }
catch (Exception ex) catch (Exception ex)
@ -98,16 +101,21 @@ public sealed class DesignManager : DesignEditor
var (actualName, path) = ParseName(name, handlePath); var (actualName, path) = ParseName(name, handlePath);
var design = new Design(Customizations, Items) var design = new Design(Customizations, Items)
{ {
CreationDate = DateTimeOffset.UtcNow, CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow, LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(), Identifier = CreateNewGuid(),
Name = actualName, Name = actualName,
Index = Designs.Count, Index = Designs.Count,
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
}; };
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design); Designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}."); Glamourer.Log.Debug($"Added new design {design.Identifier}.");
SaveService.ImmediateSave(design); SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, path); DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
return design; return design;
} }
@ -117,17 +125,22 @@ public sealed class DesignManager : DesignEditor
var (actualName, path) = ParseName(name, handlePath); var (actualName, path) = ParseName(name, handlePath);
var design = new Design(clone) var design = new Design(clone)
{ {
CreationDate = DateTimeOffset.UtcNow, CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow, LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(), Identifier = CreateNewGuid(),
Name = actualName, Name = actualName,
Index = Designs.Count, Index = Designs.Count,
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
}; };
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design); Designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design."); Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design.");
SaveService.ImmediateSave(design); SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, path); DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
return design; return design;
} }
@ -143,11 +156,12 @@ public sealed class DesignManager : DesignEditor
Name = actualName, Name = actualName,
Index = Designs.Count, Index = Designs.Count,
}; };
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design); Designs.Add(design);
Glamourer.Log.Debug( Glamourer.Log.Debug(
$"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}."); $"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}.");
SaveService.ImmediateSave(design); SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, path); DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
return design; return design;
} }
@ -176,7 +190,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Renamed design {design.Identifier}."); Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.Renamed, design, oldName); DesignChanged.Invoke(DesignChanged.Type.Renamed, design, new RenameTransaction(oldName, newName));
} }
/// <summary> Change the description of a design. </summary> /// <summary> Change the description of a design. </summary>
@ -190,9 +204,10 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); Glamourer.Log.Debug($"Changed description of design {design.Identifier}.");
DesignChanged.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) public void ChangeColor(Design design, string newColor)
{ {
var oldColor = design.Color; var oldColor = design.Color;
@ -203,7 +218,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Changed color of design {design.Identifier}."); Glamourer.Log.Debug($"Changed color of design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, oldColor); DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, new DesignColorTransaction(oldColor, newColor));
} }
/// <summary> Add a new tag to a design. The tags remain sorted. </summary> /// <summary> Add a new tag to a design. The tags remain sorted. </summary>
@ -214,10 +229,10 @@ public sealed class DesignManager : DesignEditor
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
var idx = design.Tags.IndexOf(tag); var idx = design.Tags.AsEnumerable().IndexOf(tag);
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx)); DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, new TagAddedTransaction(tag, idx));
} }
/// <summary> Remove a tag from a design by its index. </summary> /// <summary> Remove a tag from a design by its index. </summary>
@ -231,7 +246,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}.");
DesignChanged.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> /// <summary> Rename a tag from a design by its index. The tags stay sorted.</summary>
@ -246,7 +261,8 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; 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."); Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
DesignChanged.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> /// <summary> Add an associated mod to a design. </summary>
@ -258,7 +274,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}."); Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}.");
DesignChanged.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> /// <summary> Remove an associated mod from a design. </summary>
@ -270,17 +286,26 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}."); Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, new ModRemovedTransaction(mod, settings));
} }
public void UpdateMod(Design design, Mod mod, ModSettings settings) { /// <summary> Add or update an associated mod to a design. </summary>
if (!design.AssociatedMods.ContainsKey(mod)) public void UpdateMod(Design design, Mod mod, ModSettings settings)
return; {
var hasOldSettings = design.AssociatedMods.TryGetValue(mod, out var oldSettings);
design.AssociatedMods[mod] = settings; design.AssociatedMods[mod] = settings;
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Updated associated mod {mod.DirectoryName} from design {design.Identifier}."); if (hasOldSettings)
DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); {
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> /// <summary> Set the write protection status of a design. </summary>
@ -291,7 +316,7 @@ public sealed class DesignManager : DesignEditor
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected."); Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected.");
DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, value); DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, null);
} }
/// <summary> Set the quick design bar display status of a design. </summary> /// <summary> Set the quick design bar display status of a design. </summary>
@ -302,14 +327,48 @@ public sealed class DesignManager : DesignEditor
design.QuickDesign = value; design.QuickDesign = value;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar."); Glamourer.Log.Debug(
DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, value); $"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar.");
DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, null);
} }
#endregion #endregion
#region Edit Application Rules #region Edit Application Rules
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> /// <summary> Change whether to apply a specific customize value. </summary>
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
{ {
@ -319,7 +378,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, new ApplicationTransaction(idx, !value, value));
} }
/// <summary> Change whether to apply a specific equipment piece. </summary> /// <summary> Change whether to apply a specific equipment piece. </summary>
@ -331,11 +390,23 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, slot); DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, new ApplicationTransaction((slot, false), !value, value));
}
/// <summary> Change whether to apply a specific equipment piece. </summary>
public void ChangeApplyBonusItem(Design design, BonusItemFlag slot, bool value)
{
if (!design.SetApplyBonusItem(slot, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
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> /// <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)) if (!design.SetApplyStain(slot, value))
return; return;
@ -343,7 +414,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, slot); DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, new ApplicationTransaction((slot, true), !value, value));
} }
/// <summary> Change whether to apply a specific crest visibility. </summary> /// <summary> Change whether to apply a specific crest visibility. </summary>
@ -355,7 +426,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}."); Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, slot); DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, new ApplicationTransaction(slot, !value, value));
} }
/// <summary> Change the application value of one of the meta flags. </summary> /// <summary> Change the application value of one of the meta flags. </summary>
@ -367,7 +438,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}."); Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value)); DesignChanged.Invoke(DesignChanged.Type.Other, design, new ApplicationTransaction(metaIndex, !value, value));
} }
/// <summary> Change the application value of a customize parameter. </summary> /// <summary> Change the application value of a customize parameter. </summary>
@ -379,7 +450,40 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow; design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design); SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}."); Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, flag); DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, new ApplicationTransaction(flag, !value, value));
}
/// <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 (parameters is { } p)
foreach (var f in CustomizeParameterExtensions.AllFlags)
ChangeApplyParameter(design, f, p);
if (materials is { } ma)
foreach (var (key, _) in design.GetMaterialData().ToArray())
ChangeApplyMaterialValue(design, MaterialValueIndex.FromKey(key), ma);
} }
#endregion #endregion
@ -453,7 +557,7 @@ public sealed class DesignManager : DesignEditor
try try
{ {
File.Move(SaveService.FileNames.MigrationDesignFile, File.Move(SaveService.FileNames.MigrationDesignFile,
Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak")); Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak"), true);
Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file."); Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file.");
} }
catch (Exception ex) catch (Exception ex)

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

@ -13,7 +13,8 @@ public readonly record struct ApplySettings(
bool FromJobChange = false, bool FromJobChange = false,
bool UseSingleSource = false, bool UseSingleSource = false,
bool MergeLinks = false, bool MergeLinks = false,
bool ResetMaterials = false) bool ResetMaterials = false,
bool IsFinal = false)
{ {
public static readonly ApplySettings Manual = new() public static readonly ApplySettings Manual = new()
{ {
@ -24,6 +25,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false, UseSingleSource = false,
MergeLinks = false, MergeLinks = false,
ResetMaterials = false, ResetMaterials = false,
IsFinal = false,
}; };
public static readonly ApplySettings ManualWithLinks = new() public static readonly ApplySettings ManualWithLinks = new()
@ -35,6 +37,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false, UseSingleSource = false,
MergeLinks = true, MergeLinks = true,
ResetMaterials = false, ResetMaterials = false,
IsFinal = false,
}; };
public static readonly ApplySettings Game = new() public static readonly ApplySettings Game = new()
@ -46,6 +49,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false, UseSingleSource = false,
MergeLinks = false, MergeLinks = false,
ResetMaterials = true, ResetMaterials = true,
IsFinal = false,
}; };
} }
@ -64,12 +68,15 @@ public interface IDesignEditor
public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default) public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default)
=> ChangeEquip(data, slot, item, null, settings); => 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> /// <summary> Change the stain for any equipment piece. </summary>
public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings settings = default) public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings = default)
=> ChangeEquip(data, slot, null, stain, settings); => ChangeEquip(data, slot, null, stains, settings);
/// <summary> Change an equipment piece and its stain at the same time. </summary> /// <summary> Change an equipment piece and its stain at the same time. </summary>
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings settings = default); public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings = default);
/// <summary> Change the crest visibility for any equipment piece. </summary> /// <summary> Change the crest visibility for any equipment piece. </summary>
public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default); public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default);

View file

@ -2,6 +2,7 @@
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
using Glamourer.State; using Glamourer.State;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
@ -15,11 +16,16 @@ public interface IDesignStandIn : IEquatable<IDesignStandIn>
public string SerializeName(); public string SerializeName();
public StateSource AssociatedSource(); public StateSource AssociatedSource();
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags)> AllLinks { get; } public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication);
public void AddData(JObject jObj); public void AddData(JObject jObj);
public void ParseData(JObject jObj); public void ParseData(JObject jObj);
public bool ChangeData(object data); public bool ChangeData(object data);
public bool ForcedRedraw { get; }
public bool ResetAdvancedDyes { get; }
public bool ResetTemporarySettings { get; }
} }

View file

@ -1,7 +1,8 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services; using OtterGui.Services;
using Notification = OtterGui.Classes.Notification;
namespace Glamourer.Designs.Links; namespace Glamourer.Designs.Links;

View file

@ -1,4 +1,5 @@
using Glamourer.Automation; using Glamourer.Automation;
using Glamourer.Designs.History;
using Glamourer.Events; using Glamourer.Events;
using Glamourer.Services; using Glamourer.Services;
using OtterGui.Services; using OtterGui.Services;
@ -67,7 +68,7 @@ public sealed class DesignLinkManager : IService, IDisposable
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null); _event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
} }
private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, object? _) private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, ITransaction? _)
{ {
if (type is not DesignChanged.Type.Deleted) if (type is not DesignChanged.Type.Deleted)
return; return;

View file

@ -1,4 +1,6 @@
using Glamourer.Automation; using Glamourer.Api.Enums;
using Glamourer.Automation;
using Glamourer.Designs.Special;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
using Glamourer.Services; using Glamourer.Services;
@ -19,16 +21,18 @@ public class DesignMerger(
{ {
public MergedDesign Merge(LinkContainer designs, in CustomizeArray currentCustomize, in DesignData baseRef, bool respectOwnership, public MergedDesign Merge(LinkContainer designs, in CustomizeArray currentCustomize, in DesignData baseRef, bool respectOwnership,
bool modAssociations) bool modAssociations)
=> Merge(designs.Select(d => ((IDesignStandIn)d.Link, d.Type)), currentCustomize, baseRef, respectOwnership, modAssociations); => Merge(designs.Select(d => ((IDesignStandIn)d.Link, d.Type, JobFlag.All)), currentCustomize, baseRef, respectOwnership,
modAssociations);
public MergedDesign Merge(IEnumerable<(IDesignStandIn, ApplicationType)> designs, in CustomizeArray currentCustomize, in DesignData baseRef, public MergedDesign Merge(IEnumerable<(IDesignStandIn, ApplicationType, JobFlag)> designs, in CustomizeArray currentCustomize,
bool respectOwnership, bool modAssociations) in DesignData baseRef, bool respectOwnership, bool modAssociations)
{ {
var ret = new MergedDesign(designManager); var ret = new MergedDesign(designManager);
ret.Design.SetCustomize(_customize, currentCustomize); ret.Design.SetCustomize(_customize, currentCustomize);
CustomizeFlag fixFlags = 0; var startBodyType = currentCustomize.BodyType;
CustomizeFlag fixFlags = 0;
respectOwnership &= _config.UnlockedItemMode; respectOwnership &= _config.UnlockedItemMode;
foreach (var (design, type) in designs) foreach (var (design, type, jobs) in designs)
{ {
if (type is 0) if (type is 0)
continue; continue;
@ -39,17 +43,24 @@ public class DesignMerger(
if (!data.IsHuman) if (!data.IsHuman)
continue; continue;
var (equipFlags, customizeFlags, crestFlags, parameterFlags, applyMeta) = type.ApplyWhat(design); var collection = type.ApplyWhat(design);
ReduceMeta(data, applyMeta, ret, source); ReduceMeta(data, collection.Meta, ret, source);
ReduceCustomize(data, customizeFlags, ref fixFlags, ret, source, respectOwnership); ReduceCustomize(data, collection.Customize, ref fixFlags, ret, source, respectOwnership, startBodyType);
ReduceEquip(data, equipFlags, ret, source, respectOwnership); ReduceEquip(data, collection.Equip, ret, source, respectOwnership);
ReduceMainhands(data, equipFlags, ret, source, respectOwnership); ReduceBonusItems(data, collection.BonusItem, ret, source, respectOwnership);
ReduceOffhands(data, equipFlags, ret, source, respectOwnership); ReduceMainhands(data, jobs, collection.Equip, ret, source, respectOwnership);
ReduceCrests(data, crestFlags, ret, source); ReduceOffhands(data, jobs, collection.Equip, ret, source, respectOwnership);
ReduceParameters(data, parameterFlags, ret, source); ReduceCrests(data, collection.Crest, ret, source);
ReduceParameters(data, collection.Parameters, ret, source);
ReduceMods(design as Design, ret, modAssociations); ReduceMods(design as Design, ret, modAssociations);
if (type.HasFlag(ApplicationType.GearCustomization)) if (type.HasFlag(ApplicationType.GearCustomization))
ReduceMaterials(design, ret); ReduceMaterials(design, ret);
if (design.ForcedRedraw)
ret.ForcedRedraw = true;
if (design.ResetAdvancedDyes)
ret.ResetAdvancedDyes = true;
if (design.ResetTemporarySettings)
ret.ResetTemporarySettings = true;
} }
ApplyFixFlags(ret, fixFlags); ApplyFixFlags(ret, fixFlags);
@ -78,7 +89,7 @@ public class DesignMerger(
private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source) private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source)
{ {
applyMeta &= ~ret.Design.ApplyMeta; applyMeta &= ~ret.Design.Application.Meta;
if (applyMeta == 0) if (applyMeta == 0)
return; return;
@ -95,7 +106,7 @@ public class DesignMerger(
private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source) private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source)
{ {
crestFlags &= ~ret.Design.ApplyCrest; crestFlags &= ~ret.Design.Application.Crest;
if (crestFlags == 0) if (crestFlags == 0)
return; return;
@ -113,7 +124,7 @@ public class DesignMerger(
private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret, private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret,
StateSource source) StateSource source)
{ {
parameterFlags &= ~ret.Design.ApplyParameters; parameterFlags &= ~ret.Design.Application.Parameters;
if (parameterFlags == 0) if (parameterFlags == 0)
return; return;
@ -131,7 +142,7 @@ public class DesignMerger(
private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source, private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership) bool respectOwnership)
{ {
equipFlags &= ~ret.Design.ApplyEquip; equipFlags &= ~ret.Design.Application.Equip;
if (equipFlags == 0) if (equipFlags == 0)
return; return;
@ -169,7 +180,23 @@ public class DesignMerger(
} }
} }
private void ReduceMainhands(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource 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) bool respectOwnership)
{ {
if (!equipFlags.HasFlag(EquipFlag.Mainhand)) if (!equipFlags.HasFlag(EquipFlag.Mainhand))
@ -185,10 +212,11 @@ public class DesignMerger(
ret.Design.GetDesignDataRef().SetItem(EquipSlot.MainHand, weapon); ret.Design.GetDesignDataRef().SetItem(EquipSlot.MainHand, weapon);
} }
ret.Weapons.TryAdd(weapon.Type, (weapon, source)); ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs);
} }
private void ReduceOffhands(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source, bool respectOwnership) private void ReduceOffhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership)
{ {
if (!equipFlags.HasFlag(EquipFlag.Offhand)) if (!equipFlags.HasFlag(EquipFlag.Offhand))
return; return;
@ -204,14 +232,14 @@ public class DesignMerger(
} }
if (weapon.Valid) if (weapon.Valid)
ret.Weapons.TryAdd(weapon.Type, (weapon, source)); ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs);
} }
private void ReduceCustomize(in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag fixFlags, MergedDesign ret, private void ReduceCustomize(in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag fixFlags, MergedDesign ret,
StateSource source, bool respectOwnership) StateSource source, bool respectOwnership, CustomizeValue startBodyType)
{ {
customizeFlags &= ~ret.Design.ApplyCustomizeExcludingBodyType; customizeFlags &= ~ret.Design.ApplyCustomizeExcludingBodyType;
if (ret.Design.DesignData.Customize.BodyType != 1) if (ret.Design.DesignData.Customize.BodyType != startBodyType)
customizeFlags &= ~CustomizeFlag.BodyType; customizeFlags &= ~CustomizeFlag.BodyType;
if (customizeFlags == 0) if (customizeFlags == 0)

View file

@ -14,6 +14,16 @@ public sealed class LinkContainer : List<DesignLink>
public new int Count public new int Count
=> base.Count + After.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) public bool Reorder(int fromIndex, LinkOrder fromOrder, int toIndex, LinkOrder toOrder)
{ {
var fromList = fromOrder switch var fromList = fromOrder switch
@ -89,13 +99,15 @@ public sealed class LinkContainer : List<DesignLink>
if (GetAllLinks(parent).Any(l => l.Link.Link == child && l.Order != order)) 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."; 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; return false;
} }
if (GetAllLinks(child).Any(l => l.Link.Link == parent && l.Order == order)) 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."; 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; return false;
} }

View file

@ -1,22 +1,71 @@
using Glamourer.Events; using Glamourer.Interop.Penumbra;
using Glamourer.GameData;
using Glamourer.Interop.Penumbra;
using Glamourer.State; using Glamourer.State;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Links; 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 sealed class MergedDesign
{ {
public MergedDesign(DesignManager designManager) public MergedDesign(DesignManager designManager)
{ {
Design = designManager.CreateTemporary(); Design = designManager.CreateTemporary();
Design.ApplyEquip = 0; Design.Application = ApplicationCollection.None;
Design.ApplyCustomize = 0;
Design.ApplyCrest = 0;
Design.ApplyParameters = 0;
Design.ApplyMeta = 0;
} }
public MergedDesign(DesignBase design) public MergedDesign(DesignBase design)
@ -26,15 +75,17 @@ public sealed class MergedDesign
{ {
var weapon = design.DesignData.Item(EquipSlot.MainHand); var weapon = design.DesignData.Item(EquipSlot.MainHand);
if (weapon.Valid) if (weapon.Valid)
Weapons.TryAdd(weapon.Type, (weapon, StateSource.Manual)); Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All);
} }
if (design.DoApplyEquip(EquipSlot.OffHand)) if (design.DoApplyEquip(EquipSlot.OffHand))
{ {
var weapon = design.DesignData.Item(EquipSlot.OffHand); var weapon = design.DesignData.Item(EquipSlot.OffHand);
if (weapon.Valid) if (weapon.Valid)
Weapons.TryAdd(weapon.Type, (weapon, StateSource.Manual)); Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All);
} }
ForcedRedraw = design is IDesignStandIn { ForcedRedraw: true };
} }
public MergedDesign(Design design) public MergedDesign(Design design)
@ -44,8 +95,11 @@ public sealed class MergedDesign
AssociatedMods[mod] = settings; AssociatedMods[mod] = settings;
} }
public readonly DesignBase Design; public readonly DesignBase Design;
public readonly Dictionary<FullEquipType, (EquipItem, StateSource)> Weapons = new(4); public readonly WeaponList Weapons = new();
public readonly SortedList<Mod, ModSettings> AssociatedMods = []; public readonly SortedList<Mod, ModSettings> AssociatedMods = [];
public StateSources Sources = new(); public StateSources Sources = new();
public bool ForcedRedraw;
public bool ResetAdvancedDyes;
public bool ResetTemporarySettings;
} }

View file

@ -1,4 +1,5 @@
using Glamourer.State; using Glamourer.Api.Enums;
using Glamourer.State;
namespace Glamourer.Designs; namespace Glamourer.Designs;
@ -9,23 +10,15 @@ public enum MetaIndex
VisorState = StateIndex.MetaVisorState, VisorState = StateIndex.MetaVisorState,
WeaponState = StateIndex.MetaWeaponState, WeaponState = StateIndex.MetaWeaponState,
ModelId = StateIndex.MetaModelId, ModelId = StateIndex.MetaModelId,
} EarState = StateIndex.MetaEarState,
[Flags]
public enum MetaFlag : byte
{
Wetness = 0x01,
HatState = 0x02,
VisorState = 0x04,
WeaponState = 0x08,
} }
public static class MetaExtensions public static class MetaExtensions
{ {
public static readonly IReadOnlyList<MetaIndex> AllRelevant = public static readonly IReadOnlyList<MetaIndex> AllRelevant =
[MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState]; [MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState, MetaIndex.EarState];
public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState;
public static MetaFlag ToFlag(this MetaIndex index) public static MetaFlag ToFlag(this MetaIndex index)
=> index switch => index switch
@ -34,6 +27,7 @@ public static class MetaExtensions
MetaIndex.HatState => MetaFlag.HatState, MetaIndex.HatState => MetaFlag.HatState,
MetaIndex.VisorState => MetaFlag.VisorState, MetaIndex.VisorState => MetaFlag.VisorState,
MetaIndex.WeaponState => MetaFlag.WeaponState, MetaIndex.WeaponState => MetaFlag.WeaponState,
MetaIndex.EarState => MetaFlag.EarState,
_ => (MetaFlag)byte.MaxValue, _ => (MetaFlag)byte.MaxValue,
}; };
@ -44,9 +38,24 @@ public static class MetaExtensions
MetaFlag.HatState => MetaIndex.HatState, MetaFlag.HatState => MetaIndex.HatState,
MetaFlag.VisorState => MetaIndex.VisorState, MetaFlag.VisorState => MetaIndex.VisorState,
MetaFlag.WeaponState => MetaIndex.WeaponState, MetaFlag.WeaponState => MetaIndex.WeaponState,
MetaFlag.EarState => MetaIndex.EarState,
_ => (MetaIndex)byte.MaxValue, _ => (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) public static string ToName(this MetaIndex index)
=> index switch => index switch
{ {
@ -54,6 +63,7 @@ public static class MetaExtensions
MetaIndex.VisorState => "Visor Toggled", MetaIndex.VisorState => "Visor Toggled",
MetaIndex.WeaponState => "Weapon Visible", MetaIndex.WeaponState => "Weapon Visible",
MetaIndex.Wetness => "Force Wetness", MetaIndex.Wetness => "Force Wetness",
MetaIndex.EarState => "Ears Visible",
_ => "Unknown Meta", _ => "Unknown Meta",
}; };
@ -64,6 +74,7 @@ public static class MetaExtensions
MetaIndex.VisorState => "Toggle the visor state of 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.WeaponState => "Hide or show the characters weapons when not drawn.",
MetaIndex.Wetness => "Force the character to be wet or not.", 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, _ => 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

@ -2,6 +2,7 @@
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
using Glamourer.State; using Glamourer.State;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Special; namespace Glamourer.Designs.Special;
@ -11,7 +12,8 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn
public const string ResolvedName = "Random"; public const string ResolvedName = "Random";
private Design? _currentDesign; private Design? _currentDesign;
public IReadOnlyList<IDesignPredicate> Predicates { get; private set; } = []; public IReadOnlyList<IDesignPredicate> Predicates { get; private set; } = [];
public bool ResetOnRedraw { get; set; } = false;
public string ResolveName(bool _) public string ResolveName(bool _)
=> ResolvedName; => ResolvedName;
@ -39,42 +41,62 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn
public bool Equals(IDesignStandIn? other) public bool Equals(IDesignStandIn? other)
=> other is RandomDesign r => other is RandomDesign r
&& r.ResetOnRedraw == ResetOnRedraw
&& string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates), && string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates),
StringComparison.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
public StateSource AssociatedSource() public StateSource AssociatedSource()
=> StateSource.Manual; => StateSource.Manual;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags)> AllLinks public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
{ {
get if (newApplication || ResetOnRedraw)
{
_currentDesign = rng.Design(Predicates); _currentDesign = rng.Design(Predicates);
if (_currentDesign == null) else
yield break; _currentDesign ??= rng.Design(Predicates);
if (_currentDesign == null)
yield break;
foreach (var (link, type) in _currentDesign.AllLinks) foreach (var (link, type, jobs) in _currentDesign.AllLinks(newApplication))
yield return (link, type); yield return (link, type, jobs);
}
} }
public void AddData(JObject jObj) public void AddData(JObject jObj)
{ {
jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates); jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates);
jObj["ResetOnRedraw"] = ResetOnRedraw;
} }
public void ParseData(JObject jObj) public void ParseData(JObject jObj)
{ {
var restrictions = jObj["Restrictions"]?.ToObject<string>() ?? string.Empty; var restrictions = jObj["Restrictions"]?.ToObject<string>() ?? string.Empty;
Predicates = RandomPredicate.GeneratePredicates(restrictions); Predicates = RandomPredicate.GeneratePredicates(restrictions);
ResetOnRedraw = jObj["ResetOnRedraw"]?.ToObject<bool>() ?? false;
} }
public bool ChangeData(object data) public bool ChangeData(object data)
{ {
if (data is not List<IDesignPredicate> predicates) if (data is List<IDesignPredicate> predicates)
return false; {
Predicates = predicates;
return true;
}
Predicates = predicates; if (data is bool resetOnRedraw)
return true; {
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

@ -1,19 +1,33 @@
using OtterGui.Services; using OtterGui;
using OtterGui.Services;
namespace Glamourer.Designs.Special; namespace Glamourer.Designs.Special;
public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem) : IService public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem, Configuration config) : IService
{ {
private readonly Random _rng = new(); private readonly Random _rng = new();
private readonly WeakReference<Design> _lastDesign = new(null!, false);
public Design? Design(IReadOnlyList<Design> localDesigns) public Design? Design(IReadOnlyList<Design> localDesigns)
{ {
if (localDesigns.Count == 0) if (localDesigns.Count is 0)
return null; return null;
var idx = _rng.Next(0, localDesigns.Count - 1); var idx = _rng.Next(0, localDesigns.Count);
Glamourer.Log.Verbose($"[Random Design] Chose design {idx} out of {localDesigns.Count}: {localDesigns[idx].Incognito}."); if (localDesigns.Count is 1)
return localDesigns[idx]; {
_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() public Design? Design()
@ -24,12 +38,12 @@ public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileS
public Design? Design(IReadOnlyList<IDesignPredicate> predicates) public Design? Design(IReadOnlyList<IDesignPredicate> predicates)
{ {
if (predicates.Count == 0) return predicates.Count switch
return Design(); {
if (predicates.Count == 1) 0 => Design(),
return Design(predicates[0]); 1 => Design(predicates[0]),
_ => Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()),
return Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()); };
} }
public Design? Design(string restrictions) public Design? Design(string restrictions)

View file

@ -22,7 +22,7 @@ public interface IDesignPredicate
: designs; : designs;
private static (Design Design, string LowerName, string Identifier, string LowerPath) Transform(Design d, DesignFileSystem fs) private static (Design Design, string LowerName, string Identifier, string LowerPath) Transform(Design d, DesignFileSystem fs)
=> (d, d.Name.Lower, d.Identifier.ToString(), fs.FindLeaf(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty); => (d, d.Name.Lower, d.Identifier.ToString(), fs.TryGetValue(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty);
} }
public static class RandomPredicate public static class RandomPredicate

View file

@ -2,6 +2,7 @@
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
using Glamourer.State; using Glamourer.State;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Special; namespace Glamourer.Designs.Special;
@ -28,9 +29,9 @@ public class RevertDesign : IDesignStandIn
public StateSource AssociatedSource() public StateSource AssociatedSource()
=> StateSource.Game; => StateSource.Game;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags)> AllLinks public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool _)
{ {
get { yield return (this, ApplicationType.All); } yield return (this, ApplicationType.All, JobFlag.All);
} }
public void AddData(JObject jObj) public void AddData(JObject jObj)
@ -41,4 +42,13 @@ public class RevertDesign : IDesignStandIn
public bool ChangeData(object data) public bool ChangeData(object data)
=> false; => false;
public bool ForcedRedraw
=> false;
public bool ResetAdvancedDyes
=> true;
public bool ResetTemporarySettings
=> true;
} }

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ImGuiNotification;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Services; using Glamourer.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -9,15 +9,20 @@ namespace Glamourer;
public class EphemeralConfig : ISavable public class EphemeralConfig : ISavable
{ {
public int Version { get; set; } = Configuration.Constants.CurrentVersion; public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public bool IncognitoMode { get; set; } = false; public bool IncognitoMode { get; set; } = false;
public bool UnlockDetailMode { get; set; } = true; public bool UnlockDetailMode { get; set; } = true;
public bool ShowDesignQuickBar { get; set; } = false; public bool ShowDesignQuickBar { get; set; } = false;
public bool LockDesignQuickBar { get; set; } = false; public bool LockDesignQuickBar { get; set; } = false;
public bool LockMainWindow { get; set; } = false; public bool LockMainWindow { get; set; } = false;
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
public Guid SelectedDesign { get; set; } = Guid.Empty; public Guid SelectedDesign { get; set; } = Guid.Empty;
public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion; 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] [JsonIgnore]

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

@ -37,6 +37,9 @@ public sealed class AutomationChanged()
/// <summary> Change the used base state of a given set. Additional data is prior and new base. [(AutoDesignSet.Base, AutoDesignSet.Base)]. </summary> /// <summary> Change the used base state of a given set. Additional data is prior and new base. [(AutoDesignSet.Base, AutoDesignSet.Base)]. </summary>
ChangedBase, 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> /// <summary> Add a new associated design to a given set. Additional data is the index it got added at [int]. </summary>
AddedDesign, AddedDesign,

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,4 +1,5 @@
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Gui; using Glamourer.Gui;
using OtterGui.Classes; using OtterGui.Classes;
@ -13,104 +14,122 @@ namespace Glamourer.Events;
/// </list> /// </list>
/// </summary> /// </summary>
public sealed class DesignChanged() public sealed class DesignChanged()
: EventWrapper<DesignChanged.Type, Design, object?, DesignChanged.Priority>(nameof(DesignChanged)) : EventWrapper<DesignChanged.Type, Design, ITransaction?, DesignChanged.Priority>(nameof(DesignChanged))
{ {
public enum Type 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, Created,
/// <summary> An existing design was deleted. Data is null. </summary> /// <summary> An existing design was deleted. </summary>
Deleted, Deleted,
/// <summary> Invoked on full reload. Design and Data are null. </summary> /// <summary> Invoked on full reload. </summary>
ReloadedAll, ReloadedAll,
/// <summary> An existing design was renamed. Data is the prior name [string]. </summary> /// <summary> An existing design was renamed. </summary>
Renamed, 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, ChangedDescription,
/// <summary> An existing design had its associated color changed. Data is the prior color [string]. </summary> /// <summary> An existing design had its associated color changed. </summary>
ChangedColor, ChangedColor,
/// <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 a new tag added. </summary>
AddedTag, 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, 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, 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, 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, RemovedMod,
/// <summary> An existing design had a link to a different design added, removed or moved. Data is null. </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, ChangedLink,
/// <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 a customization changed. </summary>
Customize, Customize,
/// <summary> An existing design had its entire customize array changed. Data is the old array, the applied flags and the changed flags. [(CustomizeArray, CustomizeFlag, CustomizeFlag)]. </summary> /// <summary> An existing design had its entire customize array changed. </summary>
EntireCustomize, EntireCustomize,
/// <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 an equipment piece changed. </summary>
Equip, Equip,
/// <summary> An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand, the new offhand (if any) and the new gauntlets (if any). [(EquipItem, 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, 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> /// <summary> An existing design had a stain changed. </summary>
Stain, Stains,
/// <summary> An existing design had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. </summary> /// <summary> An existing design had a crest visibility changed. </summary>
Crest, Crest,
/// <summary> An existing design had a customize parameter changed. Data is the old value, the new value and the flag [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. </summary> /// <summary> An existing design had a customize parameter changed. </summary>
Parameter, Parameter,
/// <summary> An existing design had an advanced dye row added, changed, or deleted. Data is the old value, the new value and the index [(ColorRow?, ColorRow?, MaterialValueIndex)]. </summary> /// <summary> An existing design had an advanced dye row added, changed, or deleted. </summary>
Material, Material,
/// <summary> An existing design had an advanced dye rows Revert state changed. Data is the index [MaterialValueIndex]. </summary> /// <summary> An existing design had an advanced dye rows Revert state changed. </summary>
MaterialRevert, MaterialRevert,
/// <summary> An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. </summary> /// <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, ApplyCustomize,
/// <summary> An existing design changed whether a specific equipment piece 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, 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, ApplyStain,
/// <summary> An existing design changed whether a specific crest visibility is applied. Data is the slot of the equipment [EquipSlot]. </summary> /// <summary> An existing design changed whether a specific crest visibility is applied. </summary>
ApplyCrest, ApplyCrest,
/// <summary> An existing design changed whether a specific customize parameter is applied. Data is the flag for the parameter [CustomizeParameterFlag]. </summary> /// <summary> An existing design changed whether a specific customize parameter is applied. </summary>
ApplyParameter, ApplyParameter,
/// <summary> An existing design changed whether an advanced dye row is applied. Data is the index [MaterialValueIndex]. </summary> /// <summary> An existing design changed whether an advanced dye row is applied. </summary>
ApplyMaterial, ApplyMaterial,
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary> /// <summary> An existing design changed its write protection status. </summary>
WriteProtection, WriteProtection,
/// <summary> An existing design changed its display status for the quick design bar. Data is the new value [bool]. </summary> /// <summary> An existing design changed its display status for the quick design bar. </summary>
QuickDesignBar, QuickDesignBar,
/// <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 one of the meta flags. </summary>
Other, Other,
} }
public enum Priority public enum Priority
{ {
/// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChange"/> /// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChanged"/>
DesignLinkManager = 1, DesignLinkManager = 1,
/// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/> /// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/>
@ -122,7 +141,10 @@ public sealed class DesignChanged()
/// <seealso cref="Gui.Tabs.DesignTab.DesignFileSystemSelector.OnDesignChange"/> /// <seealso cref="Gui.Tabs.DesignTab.DesignFileSystemSelector.OnDesignChange"/>
DesignFileSystemSelector = -1, DesignFileSystemSelector = -1,
/// <seealso cref="SpecialDesignCombo.OnDesignChange"/> /// <seealso cref="DesignComboBase.OnDesignChanged"/>
DesignCombo = -2, DesignCombo = -2,
/// <seealso cref="EditorHistory.OnDesignChanged" />
EditorHistory = -1000,
} }
} }

View file

@ -1,6 +1,6 @@
using Glamourer.Interop.Structs;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Events; namespace Glamourer.Events;
@ -14,12 +14,12 @@ namespace Glamourer.Events;
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </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> /// </list>
/// </summary> /// </summary>
public sealed class SlotUpdating() public sealed class EquipSlotUpdating()
: EventWrapperRef34<Model, EquipSlot, CharacterArmor, ulong, SlotUpdating.Priority>(nameof(SlotUpdating)) : EventWrapperRef34<Model, EquipSlot, CharacterArmor, ulong, EquipSlotUpdating.Priority>(nameof(EquipSlotUpdating))
{ {
public enum Priority public enum Priority
{ {
/// <seealso cref="State.StateListener.OnSlotUpdating"/> /// <seealso cref="State.StateListener.OnEquipSlotUpdating"/>
StateListener = 0, StateListener = 0,
} }
} }

View file

@ -13,8 +13,8 @@ public sealed class GPoseService : EventWrapper<bool, GPoseService.Priority>
public enum Priority public enum Priority
{ {
/// <seealso cref="Api.GlamourerIpc.OnGPoseChanged"/> /// <seealso cref="Api.StateApi.OnGPoseChange"/>
GlamourerIpc = int.MinValue, StateApi = int.MinValue,
} }
public bool InGPose { get; private set; } public bool InGPose { get; private set; }

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,5 +1,5 @@
using Glamourer.Interop.Structs;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events; namespace Glamourer.Events;

View file

@ -11,7 +11,7 @@ namespace Glamourer.Events;
/// </list> /// </list>
/// </summary> /// </summary>
public sealed class MovedEquipment() public sealed class MovedEquipment()
: EventWrapper<(EquipSlot, uint, StainId)[], MovedEquipment.Priority>(nameof(MovedEquipment)) : EventWrapper<(EquipSlot, uint, StainIds)[], MovedEquipment.Priority>(nameof(MovedEquipment))
{ {
public enum Priority public enum Priority
{ {

View file

@ -12,5 +12,11 @@ public sealed class PenumbraReloaded()
{ {
/// <seealso cref="Interop.ChangeCustomizeService.Restore"/> /// <seealso cref="Interop.ChangeCustomizeService.Restore"/>
ChangeCustomizeService = 0, ChangeCustomizeService = 0,
/// <seealso cref="Interop.VisorService.Restore"/>
VisorService = 0,
/// <seealso cref="Interop.VieraEarService.Restore"/>
VieraEarService = 0,
} }
} }

View file

@ -1,67 +1,33 @@
using Glamourer.Api.Enums;
using Glamourer.Designs.History;
using Glamourer.Interop.Structs; using Glamourer.Interop.Structs;
using Glamourer.State; using Glamourer.State;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events namespace Glamourer.Events;
/// <summary>
/// Triggered when a Design is edited in any way.
/// <list type="number">
/// <item>Parameter is the type of the change </item>
/// <item>Parameter is the changed saved state. </item>
/// <item>Parameter is the existing actors using this saved state. </item>
/// <item>Parameter is any additional data depending on the type of change. </item>
/// </list>
/// </summary>
public sealed class StateChanged()
: EventWrapper<StateChangeType, StateSource, ActorState, ActorData, ITransaction?, StateChanged.Priority>(nameof(StateChanged))
{ {
/// <summary> public enum Priority
/// Triggered when a Design is edited in any way.
/// <list type="number">
/// <item>Parameter is the type of the change </item>
/// <item>Parameter is the changed saved state. </item>
/// <item>Parameter is the existing actors using this saved state. </item>
/// <item>Parameter is any additional data depending on the type of change. </item>
/// </list>
/// </summary>
public sealed class StateChanged()
: EventWrapper<StateChanged.Type, StateSource, ActorState, ActorData, object?, StateChanged.Priority>(nameof(StateChanged))
{ {
public enum Type /// <seealso cref="Api.StateApi.OnStateChanged" />
{ GlamourerIpc = int.MinValue,
/// <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> /// <seealso cref="Interop.Penumbra.PenumbraAutoRedraw.OnStateChanged" />
EntireCustomize, PenumbraAutoRedraw = 0,
/// <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> /// <seealso cref="EditorHistory.OnStateChanged" />
Customize, EditorHistory = -1000,
/// <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 crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. </summary>
Crest,
/// <summary> A characters saved state had its customize parameter changed. Data is the old value, the new value and the type [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. </summary>
Parameter,
/// <summary> A characters saved state had a material color table value changed. Data is the old value, the new value and the index [(Vector3, Vector3, MaterialValueIndex)] or just the index for resets. </summary>
MaterialValue,
/// <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,
/// <summary> A characters state was reapplied. Data is null. </summary>
Reapply,
}
public enum Priority
{
GlamourerIpc = int.MinValue,
PenumbraAutoRedraw = 0,
}
} }
} }

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

@ -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,5 +1,5 @@
using Glamourer.Interop.Structs;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events; namespace Glamourer.Events;
@ -12,11 +12,11 @@ namespace Glamourer.Events;
/// </list> /// </list>
/// </summary> /// </summary>
public sealed class VisorStateChanged() public sealed class VisorStateChanged()
: EventWrapperRef2<Model, bool, VisorStateChanged.Priority>(nameof(VisorStateChanged)) : EventWrapperRef3<Model, bool, bool, VisorStateChanged.Priority>(nameof(VisorStateChanged))
{ {
public enum Priority public enum Priority
{ {
/// <seealso cref="State.StateListener.OnVisorChange"/> /// <seealso cref="State.StateListener.OnVisorChange"/>
StateListener = 0, StateListener = 0,
} }
} }

View file

@ -1,6 +1,6 @@
using Glamourer.Interop.Structs;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Events; namespace Glamourer.Events;

View file

@ -1,5 +1,5 @@
using Glamourer.Interop.Structs;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events; namespace Glamourer.Events;

View file

@ -1,126 +0,0 @@
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.GameData;
/// <summary> A custom version of CharaMakeParams that is easier to parse. </summary>
[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);
int currentOffset;
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,4 +1,4 @@
using Dalamud.Interface.Internal; using Dalamud.Interface.Textures;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Services; using OtterGui.Services;
@ -32,8 +32,8 @@ public class CustomizeManager : IAsyncDataContainer
} }
/// <summary> Get specific icons. </summary> /// <summary> Get specific icons. </summary>
public IDalamudTextureWrap GetIcon(uint id) public ISharedImmediateTexture GetIcon(uint id)
=> _icons.LoadIcon(id)!; => _icons.TextureProvider.GetFromGameIcon(id);
/// <summary> Iterate over all supported genders and clans. </summary> /// <summary> Iterate over all supported genders and clans. </summary>
public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets() public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets()
@ -47,8 +47,8 @@ public class CustomizeManager : IAsyncDataContainer
public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet) public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet)
{ {
_icons = new IconStorage(textures, gameData); _icons = new TextureCache(gameData, textures);
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
var tmpTask = Task.Run(() => var tmpTask = Task.Run(() =>
{ {
stopwatch.Start(); stopwatch.Start();
@ -72,7 +72,7 @@ public class CustomizeManager : IAsyncDataContainer
public bool Finished public bool Finished
=> Awaiter.IsCompletedSuccessfully; => Awaiter.IsCompletedSuccessfully;
private readonly IconStorage _icons; private readonly TextureCache _icons;
private static readonly int ListSize = Clans.Count * Genders.Count; private static readonly int ListSize = Clans.Count * Genders.Count;
private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize]; private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize];

View file

@ -12,7 +12,9 @@ public struct CustomizeParameterData
public Vector3 HairSpecular; public Vector3 HairSpecular;
public Vector3 HairHighlight; public Vector3 HairHighlight;
public Vector3 LeftEye; public Vector3 LeftEye;
public float LeftLimbalIntensity;
public Vector3 RightEye; public Vector3 RightEye;
public float RightLimbalIntensity;
public Vector3 FeatureColor; public Vector3 FeatureColor;
public float FacePaintUvMultiplier; public float FacePaintUvMultiplier;
public float FacePaintUvOffset; public float FacePaintUvOffset;
@ -33,7 +35,9 @@ public struct CustomizeParameterData
CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(HairSpecular), CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(HairSpecular),
CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(HairHighlight), CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(HairHighlight),
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye), CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye),
CustomizeParameterFlag.LeftLimbalIntensity => new CustomizeParameterValue(LeftLimbalIntensity),
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye), CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye),
CustomizeParameterFlag.RightLimbalIntensity => new CustomizeParameterValue(RightLimbalIntensity),
CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(FeatureColor), CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(FeatureColor),
CustomizeParameterFlag.DecalColor => new CustomizeParameterValue(DecalColor), CustomizeParameterFlag.DecalColor => new CustomizeParameterValue(DecalColor),
CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(FacePaintUvMultiplier), CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(FacePaintUvMultiplier),
@ -57,7 +61,9 @@ public struct CustomizeParameterData
CustomizeParameterFlag.HairSpecular => SetIfDifferent(ref HairSpecular, value.InternalTriple), CustomizeParameterFlag.HairSpecular => SetIfDifferent(ref HairSpecular, value.InternalTriple),
CustomizeParameterFlag.HairHighlight => SetIfDifferent(ref HairHighlight, value.InternalTriple), CustomizeParameterFlag.HairHighlight => SetIfDifferent(ref HairHighlight, value.InternalTriple),
CustomizeParameterFlag.LeftEye => SetIfDifferent(ref LeftEye, value.InternalTriple), CustomizeParameterFlag.LeftEye => SetIfDifferent(ref LeftEye, value.InternalTriple),
CustomizeParameterFlag.LeftLimbalIntensity => SetIfDifferent(ref LeftLimbalIntensity, value.Single),
CustomizeParameterFlag.RightEye => SetIfDifferent(ref RightEye, value.InternalTriple), CustomizeParameterFlag.RightEye => SetIfDifferent(ref RightEye, value.InternalTriple),
CustomizeParameterFlag.RightLimbalIntensity => SetIfDifferent(ref RightLimbalIntensity, value.Single),
CustomizeParameterFlag.FeatureColor => SetIfDifferent(ref FeatureColor, value.InternalTriple), CustomizeParameterFlag.FeatureColor => SetIfDifferent(ref FeatureColor, value.InternalTriple),
CustomizeParameterFlag.DecalColor => SetIfDifferent(ref DecalColor, value.InternalQuadruple), CustomizeParameterFlag.DecalColor => SetIfDifferent(ref DecalColor, value.InternalQuadruple),
CustomizeParameterFlag.FacePaintUvMultiplier => SetIfDifferent(ref FacePaintUvMultiplier, value.Single), CustomizeParameterFlag.FacePaintUvMultiplier => SetIfDifferent(ref FacePaintUvMultiplier, value.Single),
@ -77,30 +83,48 @@ public struct CustomizeParameterData
_ => new CustomizeParameterValue(SkinDiffuse, MuscleTone).XivQuadruple, _ => new CustomizeParameterValue(SkinDiffuse, MuscleTone).XivQuadruple,
}; };
parameters.LeftColor = (flags & (CustomizeParameterFlag.LeftEye | CustomizeParameterFlag.FacePaintUvMultiplier)) switch parameters.LeftColor = (flags & (CustomizeParameterFlag.LeftEye | CustomizeParameterFlag.LeftLimbalIntensity)) switch
{ {
0 => parameters.LeftColor, 0 => parameters.LeftColor,
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple, CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple,
CustomizeParameterFlag.FacePaintUvMultiplier => parameters.LeftColor with { W = FacePaintUvMultiplier }, CustomizeParameterFlag.LeftLimbalIntensity => parameters.LeftColor with { W = LeftLimbalIntensity },
_ => new CustomizeParameterValue(LeftEye, FacePaintUvMultiplier).XivQuadruple, _ => new CustomizeParameterValue(LeftEye, LeftLimbalIntensity).XivQuadruple,
}; };
parameters.RightColor = (flags & (CustomizeParameterFlag.RightEye | CustomizeParameterFlag.FacePaintUvOffset)) switch parameters.RightColor = (flags & (CustomizeParameterFlag.RightEye | CustomizeParameterFlag.RightLimbalIntensity)) switch
{ {
0 => parameters.RightColor, 0 => parameters.RightColor,
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple, CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple,
CustomizeParameterFlag.FacePaintUvOffset => parameters.RightColor with { W = FacePaintUvOffset }, CustomizeParameterFlag.RightLimbalIntensity => parameters.RightColor with { W = RightLimbalIntensity },
_ => new CustomizeParameterValue(RightEye, FacePaintUvOffset).XivQuadruple, _ => new CustomizeParameterValue(RightEye, RightLimbalIntensity).XivQuadruple,
}; };
if (flags.HasFlag(CustomizeParameterFlag.SkinSpecular)) if (flags.HasFlag(CustomizeParameterFlag.SkinSpecular))
parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple; parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple;
if (flags.HasFlag(CustomizeParameterFlag.HairDiffuse)) if (flags.HasFlag(CustomizeParameterFlag.HairDiffuse))
parameters.MainColor = new CustomizeParameterValue(HairDiffuse).XivTriple; {
// 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)) if (flags.HasFlag(CustomizeParameterFlag.HairSpecular))
parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple; parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple;
if (flags.HasFlag(CustomizeParameterFlag.HairHighlight)) if (flags.HasFlag(CustomizeParameterFlag.HairHighlight))
parameters.MeshColor = new CustomizeParameterValue(HairHighlight).XivTriple; {
// 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)) if (flags.HasFlag(CustomizeParameterFlag.LipDiffuse))
parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple; parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple;
if (flags.HasFlag(CustomizeParameterFlag.FeatureColor)) if (flags.HasFlag(CustomizeParameterFlag.FeatureColor))
@ -132,13 +156,21 @@ public struct CustomizeParameterData
parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple; parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple;
break; break;
case CustomizeParameterFlag.HairDiffuse: case CustomizeParameterFlag.HairDiffuse:
parameters.MainColor = new CustomizeParameterValue(HairDiffuse).XivTriple; // 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; break;
case CustomizeParameterFlag.HairSpecular: case CustomizeParameterFlag.HairSpecular:
parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple; parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple;
break; break;
case CustomizeParameterFlag.HairHighlight: case CustomizeParameterFlag.HairHighlight:
parameters.MeshColor = new CustomizeParameterValue(HairHighlight).XivTriple; // 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; break;
case CustomizeParameterFlag.LeftEye: case CustomizeParameterFlag.LeftEye:
parameters.LeftColor = new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple; parameters.LeftColor = new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple;
@ -150,10 +182,16 @@ public struct CustomizeParameterData
parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple; parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple;
break; break;
case CustomizeParameterFlag.FacePaintUvMultiplier: case CustomizeParameterFlag.FacePaintUvMultiplier:
parameters.LeftColor.W = FacePaintUvMultiplier; GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier;
break; break;
case CustomizeParameterFlag.FacePaintUvOffset: case CustomizeParameterFlag.FacePaintUvOffset:
parameters.RightColor.W = FacePaintUvOffset; GetUvOffsetWrite(ref parameters) = FacePaintUvOffset;
break;
case CustomizeParameterFlag.LeftLimbalIntensity:
parameters.LeftColor.W = LeftLimbalIntensity;
break;
case CustomizeParameterFlag.RightLimbalIntensity:
parameters.RightColor.W = RightLimbalIntensity;
break; break;
} }
} }
@ -161,8 +199,8 @@ public struct CustomizeParameterData
public static CustomizeParameterData FromParameters(in CustomizeParameter parameter, in DecalParameters decal) public static CustomizeParameterData FromParameters(in CustomizeParameter parameter, in DecalParameters decal)
=> new() => new()
{ {
FacePaintUvOffset = parameter.RightColor.W, FacePaintUvOffset = GetUvOffset(parameter),
FacePaintUvMultiplier = parameter.LeftColor.W, FacePaintUvMultiplier = GetUvMultiplier(parameter),
MuscleTone = parameter.SkinColor.W, MuscleTone = parameter.SkinColor.W,
SkinDiffuse = new CustomizeParameterValue(parameter.SkinColor).InternalTriple, SkinDiffuse = new CustomizeParameterValue(parameter.SkinColor).InternalTriple,
SkinSpecular = new CustomizeParameterValue(parameter.SkinFresnelValue0).InternalTriple, SkinSpecular = new CustomizeParameterValue(parameter.SkinFresnelValue0).InternalTriple,
@ -171,7 +209,9 @@ public struct CustomizeParameterData
HairSpecular = new CustomizeParameterValue(parameter.HairFresnelValue0).InternalTriple, HairSpecular = new CustomizeParameterValue(parameter.HairFresnelValue0).InternalTriple,
HairHighlight = new CustomizeParameterValue(parameter.MeshColor).InternalTriple, HairHighlight = new CustomizeParameterValue(parameter.MeshColor).InternalTriple,
LeftEye = new CustomizeParameterValue(parameter.LeftColor).InternalTriple, LeftEye = new CustomizeParameterValue(parameter.LeftColor).InternalTriple,
LeftLimbalIntensity = new CustomizeParameterValue(parameter.LeftColor.W).Single,
RightEye = new CustomizeParameterValue(parameter.RightColor).InternalTriple, RightEye = new CustomizeParameterValue(parameter.RightColor).InternalTriple,
RightLimbalIntensity = new CustomizeParameterValue(parameter.RightColor.W).Single,
FeatureColor = new CustomizeParameterValue(parameter.OptionColor).InternalTriple, FeatureColor = new CustomizeParameterValue(parameter.OptionColor).InternalTriple,
DecalColor = FromParameter(decal), DecalColor = FromParameter(decal),
}; };
@ -189,8 +229,8 @@ public struct CustomizeParameterData
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(parameter.LeftColor), CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(parameter.LeftColor),
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(parameter.RightColor), CustomizeParameterFlag.RightEye => new CustomizeParameterValue(parameter.RightColor),
CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(parameter.OptionColor), CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(parameter.OptionColor),
CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(parameter.LeftColor.W), CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(GetUvMultiplier(parameter)),
CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(parameter.RightColor.W), CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(GetUvOffset(parameter)),
_ => CustomizeParameterValue.Zero, _ => CustomizeParameterValue.Zero,
}; };
@ -223,4 +263,41 @@ public struct CustomizeParameterData
val = @new; val = @new;
return true; 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

@ -16,20 +16,27 @@ public enum CustomizeParameterFlag : ushort
FacePaintUvMultiplier = 0x0400, FacePaintUvMultiplier = 0x0400,
FacePaintUvOffset = 0x0800, FacePaintUvOffset = 0x0800,
DecalColor = 0x1000, DecalColor = 0x1000,
LeftLimbalIntensity = 0x2000,
RightLimbalIntensity = 0x4000,
} }
public static class CustomizeParameterExtensions public static class CustomizeParameterExtensions
{ {
public const CustomizeParameterFlag All = (CustomizeParameterFlag)0x1FFF; // Speculars are not available anymore.
public const CustomizeParameterFlag All = (CustomizeParameterFlag)0x7FDB;
public const CustomizeParameterFlag RgbTriples = All public const CustomizeParameterFlag RgbTriples = All
& ~(RgbaQuadruples | Percentages | Values); & ~(RgbaQuadruples | Percentages | Values);
public const CustomizeParameterFlag RgbaQuadruples = CustomizeParameterFlag.DecalColor | CustomizeParameterFlag.LipDiffuse; public const CustomizeParameterFlag RgbaQuadruples = CustomizeParameterFlag.DecalColor | CustomizeParameterFlag.LipDiffuse;
public const CustomizeParameterFlag Percentages = CustomizeParameterFlag.MuscleTone;
public const CustomizeParameterFlag Percentages = CustomizeParameterFlag.MuscleTone
| CustomizeParameterFlag.LeftLimbalIntensity
| CustomizeParameterFlag.RightLimbalIntensity;
public const CustomizeParameterFlag Values = CustomizeParameterFlag.FacePaintUvOffset | CustomizeParameterFlag.FacePaintUvMultiplier; public const CustomizeParameterFlag Values = CustomizeParameterFlag.FacePaintUvOffset | CustomizeParameterFlag.FacePaintUvMultiplier;
public static readonly IReadOnlyList<CustomizeParameterFlag> AllFlags = [.. Enum.GetValues<CustomizeParameterFlag>()]; 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> 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> 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> PercentageFlags = AllFlags.Where(f => Percentages.HasFlag(f)).ToArray();
@ -56,10 +63,12 @@ public static class CustomizeParameterExtensions
CustomizeParameterFlag.HairHighlight => "Hair Highlights", CustomizeParameterFlag.HairHighlight => "Hair Highlights",
CustomizeParameterFlag.LeftEye => "Left Eye Color", CustomizeParameterFlag.LeftEye => "Left Eye Color",
CustomizeParameterFlag.RightEye => "Right Eye Color", CustomizeParameterFlag.RightEye => "Right Eye Color",
CustomizeParameterFlag.FeatureColor => "Tattoo Color", CustomizeParameterFlag.FeatureColor => "Feature Color",
CustomizeParameterFlag.FacePaintUvMultiplier => "Multiplier for Face Paint", CustomizeParameterFlag.FacePaintUvMultiplier => "Multiplier for Face Paint",
CustomizeParameterFlag.FacePaintUvOffset => "Offset of Face Paint", CustomizeParameterFlag.FacePaintUvOffset => "Offset of Face Paint",
CustomizeParameterFlag.DecalColor => "Face Paint Color", CustomizeParameterFlag.DecalColor => "Face Paint Color",
CustomizeParameterFlag.LeftLimbalIntensity => "Left Limbal Ring Intensity",
CustomizeParameterFlag.RightLimbalIntensity => "Right Limbal Ring Intensity",
_ => string.Empty, _ => string.Empty,
}; };
} }

View file

@ -1,6 +1,8 @@
using OtterGui; using OtterGui;
using OtterGui.Extensions;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.GameData; namespace Glamourer.GameData;
@ -10,12 +12,15 @@ namespace Glamourer.GameData;
/// </summary> /// </summary>
public class CustomizeSet public class CustomizeSet
{ {
internal CustomizeSet(SubRace clan, Gender gender) private readonly NpcCustomizeSet _npcCustomizations;
internal CustomizeSet(NpcCustomizeSet npcCustomizations, SubRace clan, Gender gender)
{ {
Gender = gender; _npcCustomizations = npcCustomizations;
Clan = clan; Gender = gender;
Race = clan.ToRace(); Clan = clan;
SettingAvailable = 0; Race = clan.ToRace();
SettingAvailable = 0;
} }
public Gender Gender { get; } public Gender Gender { get; }
@ -38,9 +43,9 @@ public class CustomizeSet
public string Option(CustomizeIndex index) public string Option(CustomizeIndex index)
=> OptionName[(int)index]; => OptionName[(int)index];
public IReadOnlyList<byte> Voices { get; internal init; } = null!; public IReadOnlyList<byte> Voices { get; internal init; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!; public IReadOnlyList<MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> Order { get; internal set; } = null!; public IReadOnlyDictionary<MenuType, CustomizeIndex[]> Order { get; internal set; } = null!;
// Always list selector. // Always list selector.
@ -84,6 +89,7 @@ public class CustomizeSet
{ {
if (IsAvailable(index)) if (IsAvailable(index))
return DataByValue(index, value, out custom, face) >= 0 return DataByValue(index, value, out custom, face) >= 0
|| _npcCustomizations.CheckValue(index, value)
|| NpcOptions.Any(t => t.Type == index && t.Value == value); || NpcOptions.Any(t => t.Type == index && t.Value == value);
custom = null; custom = null;
@ -97,9 +103,9 @@ public class CustomizeSet
return type switch return type switch
{ {
CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom), MenuType.ListSelector => GetInteger0(out custom),
CharaMakeParams.MenuType.List1Selector => GetInteger1(out custom), MenuType.List1Selector => GetInteger1(out custom),
CharaMakeParams.MenuType.IconSelector => index switch MenuType.IconSelector => index switch
{ {
CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom), CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom),
CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles, CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles,
@ -109,7 +115,7 @@ public class CustomizeSet
CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom), CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom),
_ => Invalid(out custom), _ => Invalid(out custom),
}, },
CharaMakeParams.MenuType.ColorPicker => index switch MenuType.ColorPicker => index switch
{ {
CustomizeIndex.SkinColor => Get(SkinColors, value, out custom), CustomizeIndex.SkinColor => Get(SkinColors, value, out custom),
CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom), CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom),
@ -121,16 +127,16 @@ public class CustomizeSet
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom), _ => Invalid(out custom),
}, },
CharaMakeParams.MenuType.DoubleColorPicker => index switch MenuType.DoubleColorPicker => index switch
{ {
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom), _ => Invalid(out custom),
}, },
CharaMakeParams.MenuType.IconCheckmark => GetBool(index, value, out custom), MenuType.IconCheckmark => GetBool(index, value, out custom),
CharaMakeParams.MenuType.Percentage => GetInteger0(out custom), MenuType.Percentage => GetInteger0(out custom),
CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom), MenuType.Checkmark => GetBool(index, value, out custom),
_ => Invalid(out custom), _ => Invalid(out custom),
}; };
int Get(IEnumerable<CustomizeData> list, CustomizeValue v, out CustomizeData? output) int Get(IEnumerable<CustomizeData> list, CustomizeValue v, out CustomizeData? output)
@ -208,10 +214,10 @@ public class CustomizeSet
switch (Types[(int)index]) switch (Types[(int)index])
{ {
case CharaMakeParams.MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); case MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); case MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx); case MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx);
case CharaMakeParams.MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx); case MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx);
} }
return index switch return index switch
@ -241,7 +247,7 @@ public class CustomizeSet
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CharaMakeParams.MenuType Type(CustomizeIndex index) public MenuType Type(CustomizeIndex index)
=> Types[(int)index]; => Types[(int)index];
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
@ -256,9 +262,9 @@ public class CustomizeSet
return Type(index) switch return Type(index) switch
{ {
CharaMakeParams.MenuType.Percentage => 101, MenuType.Percentage => 101,
CharaMakeParams.MenuType.IconCheckmark => 2, MenuType.IconCheckmark => 2,
CharaMakeParams.MenuType.Checkmark => 2, MenuType.Checkmark => 2,
_ => index switch _ => index switch
{ {
CustomizeIndex.Face => Faces.Count, CustomizeIndex.Face => Faces.Count,

View file

@ -1,9 +1,9 @@
using Dalamud; using Dalamud.Game;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race; using Race = Penumbra.GameData.Enums.Race;
@ -13,11 +13,11 @@ namespace Glamourer.GameData;
internal class CustomizeSetFactory( internal class CustomizeSetFactory(
IDataManager _gameData, IDataManager _gameData,
IPluginLog _log, IPluginLog _log,
IconStorage _icons, TextureCache _icons,
NpcCustomizeSet _npcCustomizeSet, NpcCustomizeSet _npcCustomizeSet,
ColorParameters _colors) ColorParameters _colors)
{ {
public CustomizeSetFactory(IDataManager gameData, IPluginLog log, IconStorage icons, NpcCustomizeSet npcCustomizeSet) public CustomizeSetFactory(IDataManager gameData, IPluginLog log, TextureCache icons, NpcCustomizeSet npcCustomizeSet)
: this(gameData, log, icons, npcCustomizeSet, new ColorParameters(gameData, log)) : this(gameData, log, icons, npcCustomizeSet, new ColorParameters(gameData, log))
{ } { }
@ -28,10 +28,10 @@ internal class CustomizeSetFactory(
var row = _charaMakeSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; var row = _charaMakeSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var hrothgar = race.ToRace() == Race.Hrothgar; var hrothgar = race.ToRace() == Race.Hrothgar;
// Create the initial set with all the easily accessible parameters available for anyone. // Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizeSet(race, gender) var set = new CustomizeSet(_npcCustomizeSet, race, gender)
{ {
Name = GetName(race, gender), Name = GetName(race, gender),
Voices = row.Voices, Voices = row.VoiceStruct,
HairStyles = GetHairStyles(race, gender), HairStyles = GetHairStyles(race, gender),
HairColors = hair, HairColors = hair,
SkinColors = skin, SkinColors = skin,
@ -58,7 +58,7 @@ internal class CustomizeSetFactory(
} }
/// <summary> Some data can not be set independently of the rest, so we need a post-processing step to finalize. </summary> /// <summary> Some data can not be set independently of the rest, so we need a post-processing step to finalize. </summary>
private void SetPostProcessing(CustomizeSet set, CharaMakeParams row) private void SetPostProcessing(CustomizeSet set, in CharaMakeType row)
{ {
SetAvailability(set, row); SetAvailability(set, row);
SetFacialFeatures(set, row); SetFacialFeatures(set, row);
@ -76,18 +76,16 @@ internal class CustomizeSetFactory(
CustomizeIndex.Hairstyle, CustomizeIndex.Hairstyle,
CustomizeIndex.LipColor, CustomizeIndex.LipColor,
CustomizeIndex.SkinColor, CustomizeIndex.SkinColor,
CustomizeIndex.FacePaintColor, CustomizeIndex.TailShape,
CustomizeIndex.HighlightsColor,
CustomizeIndex.HairColor,
CustomizeIndex.FacePaint,
CustomizeIndex.TattooColor,
CustomizeIndex.EyeColorLeft,
CustomizeIndex.EyeColorRight,
}; };
var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>(); var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>()
{
(CustomizeIndex.Height, CustomizeValue.Max),
};
_npcCustomizeSet.Awaiter.Wait(); _npcCustomizeSet.Awaiter.Wait();
foreach (var customize in _npcCustomizeSet.Select(s => s.Customize).Where(c => c.Clan == race && c.Gender == gender && c.BodyType.Value == 1)) foreach (var customize in _npcCustomizeSet.Select(s => s.Customize)
.Where(c => c.Clan == race && c.Gender == gender && c.BodyType.Value == 1))
{ {
foreach (var customizeIndex in customizeIndices) foreach (var customizeIndex in customizeIndices)
{ {
@ -106,10 +104,10 @@ internal class CustomizeSetFactory(
} }
private readonly ColorParameters _colorParameters = new(_gameData, _log); private readonly ColorParameters _colorParameters = new(_gameData, _log);
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet = _gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English)!; private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet = _gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English);
private readonly ExcelSheet<Lobby> _lobbySheet = _gameData.GetExcelSheet<Lobby>(ClientLanguage.English)!; private readonly ExcelSheet<Lobby> _lobbySheet = _gameData.GetExcelSheet<Lobby>(ClientLanguage.English);
private readonly ExcelSheet<HairMakeType> _hairSheet = _gameData.GetExcelSheet<HairMakeType>(ClientLanguage.English)!; private readonly ExcelSheet<RawRow> _hairSheet = _gameData.GetExcelSheet<RawRow>(ClientLanguage.English, "HairMakeType");
private readonly ExcelSheet<Tribe> _tribeSheet = _gameData.GetExcelSheet<Tribe>(ClientLanguage.English)!; private readonly ExcelSheet<Tribe> _tribeSheet = _gameData.GetExcelSheet<Tribe>(ClientLanguage.English);
// Those color pickers are shared between all races. // Those color pickers are shared between all races.
private readonly CustomizeData[] _highlightPicker = CreateColors(_colors, CustomizeIndex.HighlightsColor, 256, 192); private readonly CustomizeData[] _highlightPicker = CreateColors(_colors, CustomizeIndex.HighlightsColor, 256, 192);
@ -120,12 +118,7 @@ internal class CustomizeSetFactory(
private readonly CustomizeData[] _facePaintColorPickerLight = CreateColors(_colors, CustomizeIndex.FacePaintColor, 1152, 96, true); private readonly CustomizeData[] _facePaintColorPickerLight = CreateColors(_colors, CustomizeIndex.FacePaintColor, 1152, 96, true);
private readonly CustomizeData[] _tattooColorPicker = CreateColors(_colors, CustomizeIndex.TattooColor, 0, 192); private readonly CustomizeData[] _tattooColorPicker = CreateColors(_colors, CustomizeIndex.TattooColor, 0, 192);
private readonly ExcelSheet<CharaMakeParams> _charaMakeSheet = _gameData.Excel private readonly ExcelSheet<CharaMakeType> _charaMakeSheet = _gameData.Excel.GetSheet<CharaMakeType>();
.GetType()
.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
.MakeGenericMethod(typeof(CharaMakeParams))
.Invoke(_gameData.Excel, ["charamaketype", _gameData.Language.ToLumina(), null])! as ExcelSheet<CharaMakeParams>
?? null!;
/// <summary> Obtain available skin and hair colors for the given clan and gender. </summary> /// <summary> Obtain available skin and hair colors for the given clan and gender. </summary>
private (CustomizeData[] Skin, CustomizeData[] Hair) GetSkinHairColors(SubRace race, Gender gender) private (CustomizeData[] Skin, CustomizeData[] Hair) GetSkinHairColors(SubRace race, Gender gender)
@ -144,29 +137,28 @@ internal class CustomizeSetFactory(
private string GetName(SubRace race, Gender gender) private string GetName(SubRace race, Gender gender)
=> gender switch => gender switch
{ {
Gender.Male => _tribeSheet.GetRow((uint)race)?.Masculine.ToDalamudString().TextValue ?? race.ToName(), Gender.Male => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Masculine.ExtractText() : race.ToName(),
Gender.Female => _tribeSheet.GetRow((uint)race)?.Feminine.ToDalamudString().TextValue ?? race.ToName(), Gender.Female => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Feminine.ExtractText() : race.ToName(),
_ => "Unknown", _ => "Unknown",
}; };
/// <summary> Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. </summary> /// <summary> Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. </summary>
private CustomizeData[] GetHairStyles(SubRace race, Gender gender) private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
{ {
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender);
// Unknown30 is the number of available hairstyles. // Unknown30 is the number of available hairstyles.
var hairList = new List<CustomizeData>(row.Unknown30); var numHairs = row.ReadUInt8Column(30);
var hairList = new List<CustomizeData>(numHairs);
// Hairstyles can be found starting at Unknown66. // Hairstyles can be found starting at Unknown66.
for (var i = 0; i < row.Unknown30; ++i) for (var i = 0; i < numHairs; ++i)
{ {
var name = $"Unknown{66 + i * 9}"; // Hairs start at Unknown66.
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row) var customizeIdx = row.ReadUInt32Column(66 + i * 9);
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue) if (customizeIdx == uint.MaxValue)
continue; continue;
// Hair Row from CustomizeSheet might not be set in case of unlockable hair. // Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx); if (!_customizeSheet.TryGetRow(customizeIdx, out var hairRow))
if (hairRow == null)
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx)); hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx));
else if (_icons.IconExists(hairRow.Icon)) else if (_icons.IconExists(hairRow.Icon))
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon,
@ -177,45 +169,40 @@ internal class CustomizeSetFactory(
} }
/// <summary> Specific icons for tails or ears. </summary> /// <summary> Specific icons for tails or ears. </summary>
private CustomizeData[] GetTailEarShapes(CharaMakeParams row) private CustomizeData[] GetTailEarShapes(CharaMakeType row)
=> row.Menus.Cast<CharaMakeParams.Menu?>() => ExtractValues(row, CustomizeIndex.TailShape);
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray()
?? [];
/// <summary> Specific icons for faces. </summary> /// <summary> Specific icons for faces. </summary>
private CustomizeData[] GetFaces(CharaMakeParams row) private CustomizeData[] GetFaces(CharaMakeType row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx) => ExtractValues(row, CustomizeIndex.Face);
?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray()
?? [];
/// <summary> Specific icons for Hrothgar patterns. </summary> /// <summary> Specific icons for Hrothgar patterns. </summary>
private CustomizeData[] HrothgarFurPattern(CharaMakeParams row) private CustomizeData[] HrothgarFurPattern(CharaMakeType row)
=> row.Menus.Cast<CharaMakeParams.Menu?>() => ExtractValues(row, CustomizeIndex.LipColor);
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray() private CustomizeData[] ExtractValues(CharaMakeType row, CustomizeIndex type)
?? []; {
var data = row.CharaMakeStruct.FirstOrNull(m => m.Customize == type.ToByteAndMask().ByteIdx);
return data?.SubMenuParam.Take(data.Value.SubMenuNum).Select((v, i) => FromValueAndIndex(type, v, i)).ToArray() ?? [];
}
/// <summary> Get face paints from the hair sheet via reflection since there are also unlockable face paints. </summary> /// <summary> Get face paints from the hair sheet via reflection since there are also unlockable face paints. </summary>
private CustomizeData[] GetFacePaints(SubRace race, Gender gender) private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
{ {
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)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. // Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i) var numPaints = row.ReadUInt8Column(37);
var paintList = new List<CustomizeData>(numPaints);
for (var i = 0; i < numPaints; ++i)
{ {
// Face paints start at Unknown73. // Face paints start at Unknown73.
var name = $"Unknown{73 + i * 9}"; var customizeIdx = row.ReadUInt32Column(73 + i * 9);
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue) if (customizeIdx == uint.MaxValue)
continue; continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Face paint Row from CustomizeSheet might not be set in case of unlockable face paints. // Face paint Row from CustomizeSheet might not be set in case of unlockable face paints.
if (paintRow != null) if (_customizeSheet.TryGetRow(customizeIdx, out var paintRow))
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon, paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
(ushort)paintRow.RowId)); (ushort)paintRow.RowId));
else else
@ -226,21 +213,18 @@ internal class CustomizeSetFactory(
} }
/// <summary> Get List sizes. </summary> /// <summary> Get List sizes. </summary>
private static int GetListSize(CharaMakeParams row, CustomizeIndex index) private static int GetListSize(CharaMakeType row, CustomizeIndex index)
{ {
var gameId = index.ToByteAndMask().ByteIdx; var gameId = index.ToByteAndMask().ByteIdx;
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == gameId); var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId);
return menu?.Size ?? 0; return menu?.SubMenuNum ?? 0;
} }
/// <summary> Get generic Features. </summary> /// <summary> Get generic Features. </summary>
private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index) private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index)
{ => _customizeSheet.TryGetRow(value, out var row)
var row = _customizeSheet.GetRow(value); ? new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId)
return row == null : new CustomizeData(id, (CustomizeValue)(index + 1), value);
? new CustomizeData(id, (CustomizeValue)(index + 1), value)
: new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId);
}
/// <summary> Create generic color sets from the parameters. </summary> /// <summary> Create generic color sets from the parameters. </summary>
private static CustomizeData[] CreateColors(ColorParameters colorParameters, CustomizeIndex index, int offset, int num, private static CustomizeData[] CreateColors(ColorParameters colorParameters, CustomizeIndex index, int offset, int num,
@ -258,28 +242,27 @@ internal class CustomizeSetFactory(
} }
/// <summary> Set the specific option names for the given set of parameters. </summary> /// <summary> Set the specific option names for the given set of parameters. </summary>
private string[] GetOptionNames(CharaMakeParams row) private string[] GetOptionNames(CharaMakeType row)
{ {
var nameArray = Enum.GetValues<CustomizeIndex>().Select(c => var nameArray = Enum.GetValues<CustomizeIndex>().Select(c =>
{ {
// Find the first menu that corresponds to the Id. // Find the first menu that corresponds to the Id.
var byteId = c.ToByteAndMask().ByteIdx; var byteId = c.ToByteAndMask().ByteIdx;
var menu = row.Menus var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == byteId);
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == byteId);
if (menu == null) if (menu == null)
{ {
// If none exists and the id corresponds to highlights, set the Highlights name. // If none exists and the id corresponds to highlights, set the Highlights name.
if (c == CustomizeIndex.Highlights) if (c == CustomizeIndex.Highlights)
return string.Intern(_lobbySheet.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights"); return string.Intern(_lobbySheet.TryGetRow(237, out var text) ? text.Text.ExtractText() : "Highlights");
// Otherwise there is an error and we use the default name. // Otherwise there is an error and we use the default name.
return c.ToDefaultName(); return c.ToDefaultName();
} }
// Otherwise all is normal, get the menu name or if it does not work the default name. // Otherwise all is normal, get the menu name or if it does not work the default name.
var textRow = _lobbySheet.GetRow(menu.Value.Id); return string.Intern(_lobbySheet.TryGetRow(menu.Value.Menu.RowId, out var textRow)
return string.Intern(textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName()); ? textRow.Text.ExtractText()
: c.ToDefaultName());
}).ToArray(); }).ToArray();
// Add names for both eye colors. // Add names for both eye colors.
@ -300,7 +283,7 @@ internal class CustomizeSetFactory(
} }
/// <summary> Get the manu types for all available options. </summary> /// <summary> Get the manu types for all available options. </summary>
private static CharaMakeParams.MenuType[] GetMenuTypes(CharaMakeParams row) private static MenuType[] GetMenuTypes(CharaMakeType row)
{ {
// Set up the menu types for all customizations. // Set up the menu types for all customizations.
return Enum.GetValues<CustomizeIndex>().Select(c => return Enum.GetValues<CustomizeIndex>().Select(c =>
@ -312,13 +295,13 @@ internal class CustomizeSetFactory(
case CustomizeIndex.EyeColorLeft: case CustomizeIndex.EyeColorLeft:
case CustomizeIndex.EyeColorRight: case CustomizeIndex.EyeColorRight:
case CustomizeIndex.FacePaintColor: case CustomizeIndex.FacePaintColor:
return CharaMakeParams.MenuType.ColorPicker; return MenuType.ColorPicker;
case CustomizeIndex.BodyType: return CharaMakeParams.MenuType.Nothing; case CustomizeIndex.BodyType: return MenuType.Nothing;
case CustomizeIndex.FacePaintReversed: case CustomizeIndex.FacePaintReversed:
case CustomizeIndex.Highlights: case CustomizeIndex.Highlights:
case CustomizeIndex.SmallIris: case CustomizeIndex.SmallIris:
case CustomizeIndex.Lipstick: case CustomizeIndex.Lipstick:
return CharaMakeParams.MenuType.Checkmark; return MenuType.Checkmark;
case CustomizeIndex.FacialFeature1: case CustomizeIndex.FacialFeature1:
case CustomizeIndex.FacialFeature2: case CustomizeIndex.FacialFeature2:
case CustomizeIndex.FacialFeature3: case CustomizeIndex.FacialFeature3:
@ -327,29 +310,23 @@ internal class CustomizeSetFactory(
case CustomizeIndex.FacialFeature6: case CustomizeIndex.FacialFeature6:
case CustomizeIndex.FacialFeature7: case CustomizeIndex.FacialFeature7:
case CustomizeIndex.LegacyTattoo: case CustomizeIndex.LegacyTattoo:
return CharaMakeParams.MenuType.IconCheckmark; return MenuType.IconCheckmark;
} }
var gameId = c.ToByteAndMask().ByteIdx; var gameId = c.ToByteAndMask().ByteIdx;
// Otherwise find the first menu corresponding to the id. // Otherwise find the first menu corresponding to the id.
// If there is none, assume a list. // If there is none, assume a list.
var menu = row.Menus var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId);
.Cast<CharaMakeParams.Menu?>() var ret = (MenuType)(menu?.SubMenuType ?? (byte)MenuType.ListSelector);
.FirstOrDefault(m => m!.Value.Customize == gameId); if (c is CustomizeIndex.TailShape && ret is MenuType.ListSelector)
var ret = menu?.Type ?? CharaMakeParams.MenuType.ListSelector; ret = MenuType.List1Selector;
if (c is CustomizeIndex.TailShape && ret is CharaMakeParams.MenuType.ListSelector)
ret = CharaMakeParams.MenuType.List1Selector;
return ret; return ret;
}).ToArray(); }).ToArray();
} }
/// <summary> Set the availability of options according to actual availability. </summary> /// <summary> Set the availability of options according to actual availability. </summary>
private static void SetAvailability(CustomizeSet set, CharaMakeParams row) private static void SetAvailability(CustomizeSet set, CharaMakeType row)
{ {
// TODO: Hrothgar female
if (set is { Race: Race.Hrothgar, Gender: Gender.Female })
return;
Set(true, CustomizeIndex.Height); Set(true, CustomizeIndex.Height);
Set(set.Faces.Count > 0, CustomizeIndex.Face); Set(set.Faces.Count > 0, CustomizeIndex.Face);
Set(true, CustomizeIndex.Hairstyle); Set(true, CustomizeIndex.Hairstyle);
@ -399,7 +376,7 @@ internal class CustomizeSetFactory(
ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor; ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor;
var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray());
foreach (var type in Enum.GetValues<CharaMakeParams.MenuType>()) foreach (var type in Enum.GetValues<MenuType>())
dict.TryAdd(type, []); dict.TryAdd(type, []);
set.Order = dict; set.Order = dict;
} }
@ -423,7 +400,7 @@ internal class CustomizeSetFactory(
bool Valid(CustomizeData c) bool Valid(CustomizeData c)
{ {
var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0; var data = _customizeSheet.TryGetRow(c.CustomizeId, out var customize) ? customize.Unknown0 : 0;
return data == 0 || data == i + set.Faces.Count; return data == 0 || data == i + set.Faces.Count;
} }
} }
@ -435,7 +412,7 @@ internal class CustomizeSetFactory(
/// Create a list of lists of facial features and the legacy tattoo. /// Create a list of lists of facial features and the legacy tattoo.
/// Facial Features are bools in a bitfield, so we supply an "off" and an "on" value for simplicity of use. /// Facial Features are bools in a bitfield, so we supply an "off" and an "on" value for simplicity of use.
/// </summary> /// </summary>
private static void SetFacialFeatures(CustomizeSet set, CharaMakeParams row) private static void SetFacialFeatures(CustomizeSet set, in CharaMakeType row)
{ {
var count = set.Faces.Count; var count = set.Faces.Count;
set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count); set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count);
@ -444,14 +421,14 @@ internal class CustomizeSetFactory(
var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray(); var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray();
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
var data = row.FacialFeatureByFace[i].Icons; var data = row.FacialFeatureOption[i];
tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]); tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, (uint)data.Option1);
tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]); tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, (uint)data.Option2);
tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]); tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, (uint)data.Option3);
tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]); tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, (uint)data.Option4);
tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]); tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, (uint)data.Option5);
tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]); tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, (uint)data.Option6);
tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]); tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, (uint)data.Option7);
} }
set.FacialFeature1 = tmp[0]; set.FacialFeature1 = tmp[0];

View file

@ -0,0 +1,14 @@
namespace Glamourer.GameData;
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
}

View file

@ -1,9 +1,11 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.GameData; namespace Glamourer.GameData;
@ -35,32 +37,51 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
/// <summary> The list of data. </summary> /// <summary> The list of data. </summary>
private readonly List<NpcData> _data = []; private readonly List<NpcData> _data = [];
private readonly BitArray _hairColors = new(256);
private readonly BitArray _eyeColors = new(256);
private readonly BitArray _facepaintColors = new(256);
private readonly BitArray _tattooColors = new(256);
private readonly BitArray _facepaints = new(128);
public bool CheckValue(CustomizeIndex type, CustomizeValue value)
=> type switch
{
CustomizeIndex.HairColor => _hairColors[value.Value],
CustomizeIndex.HighlightsColor => _hairColors[value.Value],
CustomizeIndex.EyeColorLeft => _eyeColors[value.Value],
CustomizeIndex.EyeColorRight => _eyeColors[value.Value],
CustomizeIndex.FacePaintColor => _facepaintColors[value.Value],
CustomizeIndex.TattooColor => _tattooColors[value.Value],
CustomizeIndex.FacePaint when value.Value < 128 => _facepaints[value.Value],
_ => false,
};
/// <summary> Create the data when ready. </summary> /// <summary> Create the data when ready. </summary>
public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames) public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{ {
var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter); var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter);
Awaiter = waitTask.ContinueWith(_ => Awaiter = waitTask.ContinueWith(_ =>
{ {
var watch = Stopwatch.StartNew(); var watch = Stopwatch.StartNew();
var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs)); var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs));
var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames)); var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames));
FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result); FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result);
Time = watch.ElapsedMilliseconds; Time = watch.ElapsedMilliseconds;
}); })
.ContinueWith(_ => CheckFacepaintFiles(data, _facepaints));
} }
/// <summary> Create data from event NPCs. </summary> /// <summary> Create data from event NPCs. </summary>
private static List<NpcData> CreateEnpcData(IDataManager data, DictENpc eNpcs) private static List<NpcData> CreateEnpcData(IDataManager data, DictENpc eNpcs)
{ {
var enpcSheet = data.GetExcelSheet<ENpcBase>()!; var enpcSheet = data.GetExcelSheet<ENpcBase>();
var list = new List<NpcData>(eNpcs.Count); var list = new List<NpcData>(eNpcs.Count);
// Go through all event NPCs already collected into a dictionary. // Go through all event NPCs already collected into a dictionary.
foreach (var (id, name) in eNpcs) foreach (var (id, name) in eNpcs)
{ {
var row = enpcSheet.GetRow(id.Id);
// We only accept NPCs with valid names. // We only accept NPCs with valid names.
if (row == null || name.IsNullOrWhitespace()) if (!enpcSheet.TryGetRow(id.Id, out var row) || name.IsNullOrWhitespace())
continue; continue;
// Check if the customization is a valid human. // Check if the customization is a valid human.
@ -72,33 +93,17 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
{ {
Name = name, Name = name,
Customize = customize, Customize = customize,
ModelId = row.ModelChara.Row, ModelId = row.ModelChara.RowId,
Id = id, Id = id,
Kind = ObjectKind.EventNpc, Kind = ObjectKind.EventNpc,
}; };
// Event NPCs have a reference to NpcEquip but also contain the appearance in their own row. // Event NPCs have a reference to NpcEquip but also contain the appearance in their own row.
// Prefer the NpcEquip reference if it is set, otherwise use the own. // Prefer the NpcEquip reference if it is set and the own does not appear to be set, otherwise use the own.
if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip) if (row.NpcEquip.RowId != 0 && row.NpcEquip.Value is { } equip && row is { ModelBody: 0, ModelLegs: 0 })
{
ApplyNpcEquip(ref ret, equip); ApplyNpcEquip(ref ret, equip);
}
else else
{ ApplyNpcEquip(ref ret, row);
ret.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
ret.Set(1, row.ModelBody | (row.DyeBody.Row << 24));
ret.Set(2, row.ModelHands | (row.DyeHands.Row << 24));
ret.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24));
ret.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24));
ret.Set(5, row.ModelEars | (row.DyeEars.Row << 24));
ret.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24));
ret.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24));
ret.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24));
ret.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24));
ret.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48));
ret.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48));
ret.VisorToggled = row.Visor;
}
list.Add(ret); list.Add(ret);
} }
@ -109,14 +114,14 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
/// <summary> Create data from battle NPCs. </summary> /// <summary> Create data from battle NPCs. </summary>
private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames) private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{ {
var bnpcSheet = data.GetExcelSheet<BNpcBase>()!; var bnpcSheet = data.GetExcelSheet<BNpcBase>();
var list = new List<NpcData>((int)bnpcSheet.RowCount); var list = new List<NpcData>(bnpcSheet.Count);
// We go through all battle NPCs in the sheet because the dictionary refers to names. // We go through all battle NPCs in the sheet because the dictionary refers to names.
foreach (var baseRow in bnpcSheet) foreach (var baseRow in bnpcSheet)
{ {
// Only accept humans. // Only accept humans.
if (baseRow.ModelChara.Value!.Type != 1) if (baseRow.ModelChara.Value.Type != 1)
continue; continue;
var bnpcNameIds = bNpcNames[baseRow.RowId]; var bnpcNameIds = bNpcNames[baseRow.RowId];
@ -125,15 +130,15 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
continue; continue;
// Check if the customization is a valid human. // Check if the customization is a valid human.
var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!); var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value);
if (!valid) if (!valid)
continue; continue;
var equip = baseRow.NpcEquip.Value!; var equip = baseRow.NpcEquip.Value;
var ret = new NpcData var ret = new NpcData
{ {
Customize = customize, Customize = customize,
ModelId = baseRow.ModelChara.Row, ModelId = baseRow.ModelChara.RowId,
Id = baseRow.RowId, Id = baseRow.RowId,
Kind = ObjectKind.BattleNpc, Kind = ObjectKind.BattleNpc,
}; };
@ -164,6 +169,12 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
for (var i = 0; i < duplicates.Count; ++i) for (var i = 0; i < duplicates.Count; ++i)
{ {
var current = duplicates[i]; var current = duplicates[i];
_hairColors[current.Customize[CustomizeIndex.HairColor].Value] = true;
_hairColors[current.Customize[CustomizeIndex.HighlightsColor].Value] = true;
_eyeColors[current.Customize[CustomizeIndex.EyeColorLeft].Value] = true;
_eyeColors[current.Customize[CustomizeIndex.EyeColorRight].Value] = true;
_facepaintColors[current.Customize[CustomizeIndex.FacePaintColor].Value] = true;
_tattooColors[current.Customize[CustomizeIndex.TattooColor].Value] = true;
for (var j = 0; j < i; ++j) for (var j = 0; j < i; ++j)
{ {
if (current.DataEquals(duplicates[j])) if (current.DataEquals(duplicates[j]))
@ -202,18 +213,36 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
/// <summary> Apply equipment from a NpcEquip row. </summary> /// <summary> Apply equipment from a NpcEquip row. </summary>
private static void ApplyNpcEquip(ref NpcData data, NpcEquip row) private static void ApplyNpcEquip(ref NpcData data, NpcEquip row)
{ {
data.Set(0, row.ModelHead | (row.DyeHead.Row << 24)); data.Set(0, row.ModelHead | (row.DyeHead.RowId << 24) | ((ulong)row.Dye2Head.RowId << 32));
data.Set(1, row.ModelBody | (row.DyeBody.Row << 24)); data.Set(1, row.ModelBody | (row.DyeBody.RowId << 24) | ((ulong)row.Dye2Body.RowId << 32));
data.Set(2, row.ModelHands | (row.DyeHands.Row << 24)); data.Set(2, row.ModelHands | (row.DyeHands.RowId << 24) | ((ulong)row.Dye2Hands.RowId << 32));
data.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24)); data.Set(3, row.ModelLegs | (row.DyeLegs.RowId << 24) | ((ulong)row.Dye2Legs.RowId << 32));
data.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24)); data.Set(4, row.ModelFeet | (row.DyeFeet.RowId << 24) | ((ulong)row.Dye2Feet.RowId << 32));
data.Set(5, row.ModelEars | (row.DyeEars.Row << 24)); data.Set(5, row.ModelEars | (row.DyeEars.RowId << 24) | ((ulong)row.Dye2Ears.RowId << 32));
data.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24)); data.Set(6, row.ModelNeck | (row.DyeNeck.RowId << 24) | ((ulong)row.Dye2Neck.RowId << 32));
data.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24)); data.Set(7, row.ModelWrists | (row.DyeWrists.RowId << 24) | ((ulong)row.Dye2Wrists.RowId << 32));
data.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24)); data.Set(8, row.ModelRightRing | (row.DyeRightRing.RowId << 24) | ((ulong)row.Dye2RightRing.RowId << 32));
data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24)); data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.RowId << 24) | ((ulong)row.Dye2LeftRing.RowId << 32));
data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48)); data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.RowId << 48) | ((ulong)row.Dye2MainHand.RowId << 56));
data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48)); data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.RowId << 48) | ((ulong)row.Dye2OffHand.RowId << 56));
data.VisorToggled = row.Visor;
}
/// <summary> Apply equipment from a ENpcBase Row row. </summary>
private static void ApplyNpcEquip(ref NpcData data, ENpcBase row)
{
data.Set(0, row.ModelHead | (row.DyeHead.RowId << 24) | ((ulong)row.Dye2Head.RowId << 32));
data.Set(1, row.ModelBody | (row.DyeBody.RowId << 24) | ((ulong)row.Dye2Body.RowId << 32));
data.Set(2, row.ModelHands | (row.DyeHands.RowId << 24) | ((ulong)row.Dye2Hands.RowId << 32));
data.Set(3, row.ModelLegs | (row.DyeLegs.RowId << 24) | ((ulong)row.Dye2Legs.RowId << 32));
data.Set(4, row.ModelFeet | (row.DyeFeet.RowId << 24) | ((ulong)row.Dye2Feet.RowId << 32));
data.Set(5, row.ModelEars | (row.DyeEars.RowId << 24) | ((ulong)row.Dye2Ears.RowId << 32));
data.Set(6, row.ModelNeck | (row.DyeNeck.RowId << 24) | ((ulong)row.Dye2Neck.RowId << 32));
data.Set(7, row.ModelWrists | (row.DyeWrists.RowId << 24) | ((ulong)row.Dye2Wrists.RowId << 32));
data.Set(8, row.ModelRightRing | (row.DyeRightRing.RowId << 24) | ((ulong)row.Dye2RightRing.RowId << 32));
data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.RowId << 24) | ((ulong)row.Dye2LeftRing.RowId << 32));
data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.RowId << 48) | ((ulong)row.Dye2MainHand.RowId << 56));
data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.RowId << 48) | ((ulong)row.Dye2OffHand.RowId << 56));
data.VisorToggled = row.Visor; data.VisorToggled = row.Visor;
} }
@ -221,11 +250,11 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize) private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize)
{ {
var customize = new CustomizeArray(); var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue)(byte)bnpcCustomize.Race.Row); customize.SetByIndex(0, (CustomizeValue)(byte)bnpcCustomize.Race.RowId);
customize.SetByIndex(1, (CustomizeValue)bnpcCustomize.Gender); customize.SetByIndex(1, (CustomizeValue)bnpcCustomize.Gender);
customize.SetByIndex(2, (CustomizeValue)bnpcCustomize.BodyType); customize.SetByIndex(2, (CustomizeValue)bnpcCustomize.BodyType);
customize.SetByIndex(3, (CustomizeValue)bnpcCustomize.Height); customize.SetByIndex(3, (CustomizeValue)bnpcCustomize.Height);
customize.SetByIndex(4, (CustomizeValue)(byte)bnpcCustomize.Tribe.Row); customize.SetByIndex(4, (CustomizeValue)(byte)bnpcCustomize.Tribe.RowId);
customize.SetByIndex(5, (CustomizeValue)bnpcCustomize.Face); customize.SetByIndex(5, (CustomizeValue)bnpcCustomize.Face);
customize.SetByIndex(6, (CustomizeValue)bnpcCustomize.HairStyle); customize.SetByIndex(6, (CustomizeValue)bnpcCustomize.HairStyle);
customize.SetByIndex(7, (CustomizeValue)bnpcCustomize.HairHighlight); customize.SetByIndex(7, (CustomizeValue)bnpcCustomize.HairHighlight);
@ -259,15 +288,15 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
/// <summary> Obtain customizations from a ENpcBase row and check if the human is valid. </summary> /// <summary> Obtain customizations from a ENpcBase row and check if the human is valid. </summary>
private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase) private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase)
{ {
if (enpcBase.ModelChara.Value?.Type != 1) if (enpcBase.ModelChara.ValueNullable?.Type != 1)
return (false, CustomizeArray.Default); return (false, CustomizeArray.Default);
var customize = new CustomizeArray(); var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.Row); customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.RowId);
customize.SetByIndex(1, (CustomizeValue)enpcBase.Gender); customize.SetByIndex(1, (CustomizeValue)enpcBase.Gender);
customize.SetByIndex(2, (CustomizeValue)enpcBase.BodyType); customize.SetByIndex(2, (CustomizeValue)enpcBase.BodyType);
customize.SetByIndex(3, (CustomizeValue)enpcBase.Height); customize.SetByIndex(3, (CustomizeValue)enpcBase.Height);
customize.SetByIndex(4, (CustomizeValue)(byte)enpcBase.Tribe.Row); customize.SetByIndex(4, (CustomizeValue)(byte)enpcBase.Tribe.RowId);
customize.SetByIndex(5, (CustomizeValue)enpcBase.Face); customize.SetByIndex(5, (CustomizeValue)enpcBase.Face);
customize.SetByIndex(6, (CustomizeValue)enpcBase.HairStyle); customize.SetByIndex(6, (CustomizeValue)enpcBase.HairStyle);
customize.SetByIndex(7, (CustomizeValue)enpcBase.HairHighlight); customize.SetByIndex(7, (CustomizeValue)enpcBase.HairHighlight);
@ -298,6 +327,17 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
return (true, customize); return (true, customize);
} }
/// <summary> Check decal files for existence. </summary>
private static void CheckFacepaintFiles(IDataManager data, BitArray facepaints)
{
for (byte i = 0; i < 128; ++i)
{
var path = GamePaths.Tex.FaceDecal(i);
if (data.FileExists(path))
facepaints[i] = true;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<NpcData> GetEnumerator() public IEnumerator<NpcData> GetEnumerator()
=> _data.GetEnumerator(); => _data.GetEnumerator();

View file

@ -13,7 +13,7 @@ public unsafe struct NpcData
public CustomizeArray Customize; public CustomizeArray Customize;
/// <summary> The equipment appearance of the NPC, 10 * CharacterArmor. </summary> /// <summary> The equipment appearance of the NPC, 10 * CharacterArmor. </summary>
private fixed byte _equip[40]; private fixed byte _equip[CharacterArmor.Size * 10];
/// <summary> The mainhand weapon appearance of the NPC. </summary> /// <summary> The mainhand weapon appearance of the NPC. </summary>
public CharacterWeapon Mainhand; public CharacterWeapon Mainhand;
@ -54,36 +54,35 @@ public unsafe struct NpcData
{ {
sb.Append(span[i].Set.Id.ToString("D4")) sb.Append(span[i].Set.Id.ToString("D4"))
.Append('-') .Append('-')
.Append(span[i].Variant.Id.ToString("D3")) .Append(span[i].Variant.Id.ToString("D3"));
.Append('-') foreach (var stain in span[i].Stains)
.Append(span[i].Stain.Id.ToString("D3")) sb.Append('-').Append(stain.Id.ToString("D3"));
.Append(", ");
} }
sb.Append(Mainhand.Skeleton.Id.ToString("D4")) sb.Append(Mainhand.Skeleton.Id.ToString("D4"))
.Append('-') .Append('-')
.Append(Mainhand.Weapon.Id.ToString("D4")) .Append(Mainhand.Weapon.Id.ToString("D4"))
.Append('-') .Append('-')
.Append(Mainhand.Variant.Id.ToString("D3")) .Append(Mainhand.Variant.Id.ToString("D3"));
.Append('-') foreach (var stain in Mainhand.Stains)
.Append(Mainhand.Stain.Id.ToString("D4")) sb.Append('-').Append(stain.Id.ToString("D3"));
.Append(", ") sb.Append(", ")
.Append(Offhand.Skeleton.Id.ToString("D4")) .Append(Offhand.Skeleton.Id.ToString("D4"))
.Append('-') .Append('-')
.Append(Offhand.Weapon.Id.ToString("D4")) .Append(Offhand.Weapon.Id.ToString("D4"))
.Append('-') .Append('-')
.Append(Offhand.Variant.Id.ToString("D3")) .Append(Offhand.Variant.Id.ToString("D3"));
.Append('-') foreach (var stain in Mainhand.Stains)
.Append(Offhand.Stain.Id.ToString("D3")); sb.Append('-').Append(stain.Id.ToString("D3"));
return sb.ToString(); return sb.ToString();
} }
/// <summary> Set an equipment piece to a given value. </summary> /// <summary> Set an equipment piece to a given value. </summary>
internal void Set(int idx, uint value) internal void Set(int idx, ulong value)
{ {
fixed (byte* ptr = _equip) fixed (byte* ptr = _equip)
{ {
((uint*)ptr)[idx] = value; ((ulong*)ptr)[idx] = value;
} }
} }

View file

@ -1,5 +1,7 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using Glamourer.Api; using Glamourer.Api;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.Services; using Glamourer.Services;
@ -7,6 +9,7 @@ using Glamourer.State;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Log; using OtterGui.Log;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.GameData.Interop;
namespace Glamourer; namespace Glamourer;
@ -23,15 +26,17 @@ public class Glamourer : IDalamudPlugin
public static readonly Logger Log = new(); public static readonly Logger Log = new();
public static MessageService Messager { get; private set; } = null!; public static MessageService Messager { get; private set; } = null!;
public static DynamisIpc Dynamis { get; private set; } = null!;
private readonly ServiceManager _services; private readonly ServiceManager _services;
public Glamourer(DalamudPluginInterface pluginInterface) public Glamourer(IDalamudPluginInterface pluginInterface)
{ {
try try
{ {
_services = ServiceManagerA.CreateProvider(pluginInterface, Log); _services = StaticServiceManager.CreateProvider(pluginInterface, Log, this);
Messager = _services.GetService<MessageService>(); Messager = _services.GetService<MessageService>();
Dynamis = _services.GetService<DynamisIpc>();
_services.EnsureRequiredServices(); _services.EnsureRequiredServices();
_services.GetService<VisorService>(); _services.GetService<VisorService>();
@ -40,7 +45,7 @@ public class Glamourer : IDalamudPlugin
_services.GetService<StateListener>(); // Initialize State Listener. _services.GetService<StateListener>(); // Initialize State Listener.
_services.GetService<GlamourerWindowSystem>(); // initialize ui. _services.GetService<GlamourerWindowSystem>(); // initialize ui.
_services.GetService<CommandService>(); // initialize commands. _services.GetService<CommandService>(); // initialize commands.
_services.GetService<GlamourerIpc>(); // initialize IPC. _services.GetService<IpcProviders>(); // initialize IPC.
Log.Information($"Glamourer v{Version} loaded successfully."); Log.Information($"Glamourer v{Version} loaded successfully.");
} }
catch catch
@ -50,6 +55,96 @@ public class Glamourer : IDalamudPlugin
} }
} }
public string GatherSupportInformation()
{
var sb = new StringBuilder(10240);
var config = _services.GetService<Configuration>();
sb.AppendLine("**Settings**");
sb.Append($"> **`Plugin Version: `** {Version}\n");
sb.Append($"> **`Commit Hash: `** {CommitHash}\n");
sb.Append($"> **`Enable Auto Designs: `** {config.EnableAutoDesigns}\n");
sb.Append($"> **`Gear Protection: `** {config.UseRestrictedGearProtection}\n");
sb.Append($"> **`Item Restriction: `** {config.UnlockedItemMode}\n");
sb.Append($"> **`Keep Manual Changes: `** {config.RespectManualOnAutomationUpdate}\n");
sb.Append($"> **`Auto-Reload Gear: `** {config.AutoRedrawEquipOnChanges}\n");
sb.Append($"> **`Revert on Zone Change:`** {config.RevertManualChangesOnZoneChange}\n");
sb.Append($"> **`Festival Easter-Eggs: `** {config.DisableFestivals}\n");
sb.Append($"> **`Apply Entire Weapon: `** {config.ChangeEntireItem}\n");
sb.Append($"> **`Apply Associated Mods:`** {config.AlwaysApplyAssociatedMods}\n");
sb.Append($"> **`Attach to PCP: `** {config.AttachToPcp}\n");
sb.Append($"> **`Hidden Panels: `** {config.HideDesignPanel}\n");
sb.Append($"> **`Show QDB: `** {config.Ephemeral.ShowDesignQuickBar}\n");
sb.Append($"> **`QDB Hotkey: `** {config.ToggleQuickDesignBar}\n");
sb.Append($"> **`Smaller Equip Display:`** {config.SmallEquip}\n");
sb.Append($"> **`Debug Mode: `** {config.DebugMode}\n");
sb.Append($"> **`Cheat Codes: `** {(ulong)_services.GetService<CodeService>().AllEnabled:X8}\n");
sb.AppendLine("**Plugins**");
GatherRelevantPlugins(sb);
var designManager = _services.GetService<DesignManager>();
var autoManager = _services.GetService<AutoDesignManager>();
var stateManager = _services.GetService<StateManager>();
var objectManager = _services.GetService<ActorObjectManager>();
var currentPlayer = objectManager.PlayerData.Identifier;
var states = stateManager.Where(kvp => objectManager.ContainsKey(kvp.Key)).ToList();
sb.AppendLine("**Statistics**");
sb.Append($"> **`Current Player: `** {(currentPlayer.IsValid ? currentPlayer.Incognito(null) : "None")}\n");
sb.Append($"> **`Saved Designs: `** {designManager.Designs.Count}\n");
sb.Append($"> **`Automation Sets: `** {autoManager.Count} ({autoManager.Count(set => set.Enabled)} Enabled)\n");
sb.Append(
$"> **`Actor States: `** {stateManager.Count} ({states.Count} Visible, {stateManager.Values.Count(s => s.IsLocked)} Locked)\n");
var enabledAutomation = autoManager.Where(s => s.Enabled).ToList();
if (enabledAutomation.Count > 0)
{
sb.AppendLine("**Enabled Automation**");
foreach (var set in enabledAutomation)
{
sb.Append(
$"> **`{set.Identifiers.First().Incognito(null) + ':',-24}`** {(set.Name.Length >= 2 ? $"{set.Name.AsSpan(0, 2)}..." : set.Name)} ({set.Designs.Count} {(set.Designs.Count == 1 ? "Design" : "Designs")})\n");
}
}
if (states.Count > 0)
{
sb.AppendLine("**State**");
foreach (var (ident, state) in states)
{
var sources = Enum.GetValues<StateSource>().Select(s => (0, s)).ToArray();
foreach (var source in StateIndex.All.Select(s => state.Sources[s]))
++sources[(int)source].Item1;
foreach (var material in state.Materials.Values)
++sources[(int)material.Value.Source].Item1;
var sourcesString = string.Join(", ", sources.Where(s => s.Item1 > 0).Select(s => $"{s.s} {s.Item1}"));
sb.Append(
$"> **`{ident.Incognito(null) + ':',-24}`** {(state.IsLocked ? "Locked, " : string.Empty)}Job {state.LastJob.Id}, Zone {state.LastTerritory}, Materials {state.Materials.Values.Count}, {sourcesString}\n");
}
}
return sb.ToString();
}
private void GatherRelevantPlugins(StringBuilder sb)
{
ReadOnlySpan<string> relevantPlugins =
[
"Penumbra", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge",
"LoporritSync", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud",
];
var plugins = _services.GetService<IDalamudPluginInterface>().InstalledPlugins
.GroupBy(p => p.InternalName)
.ToDictionary(g => g.Key, g =>
{
var item = g.OrderByDescending(p => p.IsLoaded).ThenByDescending(p => p.Version).First();
return (item.IsLoaded, item.Version, item.Name);
});
foreach (var plugin in relevantPlugins)
{
if (plugins.TryGetValue(plugin, out var data))
sb.Append($"> **`{data.Name + ':',-22}`** {data.Version}{(data.IsLoaded ? string.Empty : " (Disabled)")}\n");
}
}
public void Dispose() public void Dispose()
=> _services?.Dispose(); => _services?.Dispose();

View file

@ -1,94 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Dalamud.NET.Sdk/14.0.1">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>Glamourer</RootNamespace> <RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer</AssemblyName> <AssemblyName>Glamourer</AssemblyName>
<FileVersion>9.0.0.1</FileVersion> <FileVersion>9.0.0.1</FileVersion>
<AssemblyVersion>9.0.0.1</AssemblyVersion> <AssemblyVersion>9.0.0.1</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product> <Product>Glamourer</Product>
<Copyright>Copyright © 2023</Copyright> <Copyright>Copyright © 2025</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath> <OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="LegacyTattoo.raw" /> <None Remove="LegacyTattoo.raw" />
<None Include="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="LegacyTattoo.raw" /> <EmbeddedResource Include="LegacyTattoo.raw" />
</ItemGroup> </ItemGroup>
<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="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(DalamudLibPath)ImGuiScene.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>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Glamourer.Api\Glamourer.Api.csproj" />
<ProjectReference Include="..\OtterGui\OtterGui.csproj" /> <ProjectReference Include="..\OtterGui\OtterGui.csproj" />
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" /> <ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.string.csproj" /> <ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" /> <ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Dalamud.ContextMenu" Version="1.3.1" />
<PackageReference Include="Vortice.Direct3D11" Version="3.4.2-beta" /> <PackageReference Include="Vortice.Direct3D11" Version="3.4.2-beta" />
</ItemGroup> </ItemGroup>
@ -117,14 +56,4 @@
<InformationalVersion>$(GitCommitHash)</InformationalVersion> <InformationalVersion>$(GitCommitHash)</InformationalVersion>
</PropertyGroup> </PropertyGroup>
</Target> </Target>
<ItemGroup>
<None Update="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
</Target>
</Project> </Project>

View file

@ -8,7 +8,7 @@
"AssemblyVersion": "9.0.0.1", "AssemblyVersion": "9.0.0.1",
"RepoUrl": "https://github.com/Ottermandias/Glamourer", "RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"DalamudApiLevel": 9, "DalamudApiLevel": 14,
"ImageUrls": null, "ImageUrls": null,
"IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/master/images/icon.png" "IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/master/images/icon.png"
} }

View file

@ -1,4 +1,4 @@
using ImGuiNET; using Dalamud.Bindings.ImGui;
namespace Glamourer.Gui; namespace Glamourer.Gui;
@ -29,6 +29,10 @@ public enum ColorId
TriStateNeutral, TriStateNeutral,
BattleNpc, BattleNpc,
EventNpc, EventNpc,
ModdedItemMarker,
ContainsItemsEnabled,
ContainsItemsDisabled,
AdvancedDyeActive,
} }
public static class Colors public static class Colors
@ -39,32 +43,36 @@ public static class Colors
=> color switch => color switch
{ {
// @formatter:off // @formatter:off
ColorId.NormalDesign => (0xFFFFFFFF, "Normal Design", "A design with no specific traits." ), ColorId.NormalDesign => (0xFFFFFFFF, "Normal Design", "A design with no specific traits." ),
ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ),
ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that does not change equipment or customizations on a character." ), ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that does not change equipment or customizations on a character." ),
ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ),
ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ),
ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ),
ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ), ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ),
ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ), ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ),
ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ), ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ),
ColorId.EnabledAutoSet => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once." ), ColorId.EnabledAutoSet => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once." ),
ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ), ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ),
ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ), ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ),
ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ), ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ),
ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ), ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ),
ColorId.FavoriteStarOn => (0xFF40D0D0, "Favored Item", "The color of the star for favored items and of the border in the unlock overview tab." ), ColorId.FavoriteStarOn => (0xFF40D0D0, "Favored Item", "The color of the star for favored items and of the border in the unlock overview tab." ),
ColorId.FavoriteStarHovered => (0xFFD040D0, "Favorite Star Hovered", "The color of the star for favored items when it is hovered." ), ColorId.FavoriteStarHovered => (0xFFD040D0, "Favorite Star Hovered", "The color of the star for favored items when it is hovered." ),
ColorId.FavoriteStarOff => (0x20808080, "Favorite Star Outline", "The color of the star for items that are not favored when it is not hovered." ), ColorId.FavoriteStarOff => (0x20808080, "Favorite Star Outline", "The color of the star for items that are not favored when it is not hovered." ),
ColorId.QuickDesignButton => (0x900A0A0A, "Quick Design Bar Button Background", "The color of button frames in the quick design bar." ), ColorId.QuickDesignButton => (0x900A0A0A, "Quick Design Bar Button Background", "The color of button frames in the quick design bar." ),
ColorId.QuickDesignFrame => (0x90383838, "Quick Design Bar Combo Background", "The color of the combo background in the quick design bar." ), ColorId.QuickDesignFrame => (0x90383838, "Quick Design Bar Combo Background", "The color of the combo background in the quick design bar." ),
ColorId.QuickDesignBg => (0x00F0F0F0, "Quick Design Bar Window Background", "The color of the window background in the quick design bar." ), ColorId.QuickDesignBg => (0x00F0F0F0, "Quick Design Bar Window Background", "The color of the window background in the quick design bar." ),
ColorId.TriStateCheck => (0xFF00D000, "Checkmark in Tri-State Checkboxes", "The color of the checkmark indicating positive change in tri-state checkboxes." ), ColorId.TriStateCheck => (0xFF00D000, "Checkmark in Tri-State Checkboxes", "The color of the checkmark indicating positive change in tri-state checkboxes." ),
ColorId.TriStateCross => (0xFF0000D0, "Cross in Tri-State Checkboxes", "The color of the cross indicating negative change in tri-state checkboxes." ), ColorId.TriStateCross => (0xFF0000D0, "Cross in Tri-State Checkboxes", "The color of the cross indicating negative change in tri-state checkboxes." ),
ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes." ), ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes." ),
ColorId.BattleNpc => (0xFFFFFFFF, "Battle NPC in NPC Tab", "The color of the names of battle NPCs in the NPC tab that do not have a more specific color assigned." ), ColorId.BattleNpc => (0xFFFFFFFF, "Battle NPC in NPC Tab", "The color of the names of battle NPCs in the NPC tab that do not have a more specific color assigned." ),
ColorId.EventNpc => (0xFFFFFFFF, "Event NPC in NPC Tab", "The color of the names of event NPCs in the NPC tab that do not have a more specific color assigned." ), ColorId.EventNpc => (0xFFFFFFFF, "Event NPC in NPC Tab", "The color of the names of event NPCs in the NPC tab that do not have a more specific color assigned." ),
_ => (0x00000000, string.Empty, string.Empty ), ColorId.ModdedItemMarker => (0xFFFF20FF, "Modded Item Marker", "The color of dot in the unlocks overview tab signaling that the item is modded in the currently selected Penumbra collection." ),
ColorId.ContainsItemsEnabled => (0xFFA0F0A0, "Enabled Mod Contains Design Items", "The color of enabled mods in the associated mod dropdown menu when they contain items used in this design." ),
ColorId.ContainsItemsDisabled => (0x80A0F0A0, "Disabled Mod Contains Design Items", "The color of disabled mods in the associated mod dropdown menu when they contain items used in this design." ),
ColorId.AdvancedDyeActive => (0xFF58DDFF, "Advanced Dyes Active", "The highlight color for the advanced dye button and marker if any advanced dyes are active for this slot." ),
_ => (0x00000000, string.Empty, string.Empty ),
// @formatter:on // @formatter:on
}; };

View file

@ -1,16 +1,83 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Glamourer.GameData; using Glamourer.GameData;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using System;
namespace Glamourer.Gui.Customization; namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer public partial class CustomizationDrawer
{ {
private const string ColorPickerPopupName = "ColorPicker"; private const string ColorPickerPopupName = "ColorPicker";
private CustomizeValue _draggedColorValue;
private CustomizeIndex _draggedColorType;
private void DrawDragDropSource(CustomizeIndex index, CustomizeData custom)
{
using var dragDropSource = ImUtf8.DragDropSource();
if (!dragDropSource)
return;
if (!DragDropSource.SetPayload("##colorDragDrop"u8))
_draggedColorValue = _customize[index];
ImUtf8.Text(
$"Dragging {(custom.Color == 0 ? $"{_currentOption} (NPC)" : _currentOption)} #{_draggedColorValue.Value}...");
_draggedColorType = index;
}
private void DrawDragDropTarget(CustomizeIndex index)
{
using var dragDropTarget = ImUtf8.DragDropTarget();
if (!dragDropTarget.Success || !dragDropTarget.IsDropping("##colorDragDrop"u8))
return;
var idx = _set.DataByValue(_draggedColorType, _draggedColorValue, out var draggedData, _customize.Face);
var bestMatch = _draggedColorValue;
if (draggedData.HasValue)
{
var draggedColor = draggedData.Value.Color;
var targetData = _set.Data(index, idx);
if (targetData.Color != draggedColor)
{
var bestDiff = Diff(targetData.Color, draggedColor);
var count = _set.Count(index);
for (var i = 0; i < count; ++i)
{
targetData = _set.Data(index, i);
if (targetData.Color == draggedColor)
{
UpdateValue(_draggedColorValue);
return;
}
var diff = Diff(targetData.Color, draggedColor);
if (diff >= bestDiff)
continue;
bestDiff = diff;
bestMatch = (CustomizeValue)i;
}
}
}
UpdateValue(bestMatch);
return;
static uint Diff(uint color1, uint color2)
{
var r = (color1 & 0xFF) - (color2 & 0xFF);
var g = ((color1 >> 8) & 0xFF) - ((color2 >> 8) & 0xFF);
var b = ((color1 >> 16) & 0xFF) - ((color2 >> 16) & 0xFF);
return 30 * r * r + 59 * g * g + 11 * b * b;
}
}
private void DrawColorPicker(CustomizeIndex index) private void DrawColorPicker(CustomizeIndex index)
{ {
@ -21,13 +88,18 @@ public partial class CustomizationDrawer
using (_ = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0)) using (_ = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0))
{ {
if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.NoDragDrop, _framedIconSize))
{
ImGui.OpenPopup(ColorPickerPopupName); ImGui.OpenPopup(ColorPickerPopupName);
else if (current >= 0 && CaptureMouseWheel(ref current, 0, _currentCount)) }
else if (current >= 0 && !_locked && CaptureMouseWheel(ref current, 0, _currentCount))
{ {
var data = _set.Data(_currentIndex, current, _customize.Face); var data = _set.Data(_currentIndex, current, _customize.Face);
UpdateValue(data.Value); UpdateValue(data.Value);
} }
DrawDragDropSource(index, custom);
DrawDragDropTarget(index);
} }
var npc = false; var npc = false;
@ -70,7 +142,7 @@ public partial class CustomizationDrawer
for (var i = 0; i < _currentCount; ++i) for (var i = 0; i < _currentCount; ++i)
{ {
var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]); var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]);
if (ImGui.ColorButton(custom.Value.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) if (ImGui.ColorButton(custom.Value.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)) && !_locked)
{ {
UpdateValue(custom.Value); UpdateValue(custom.Value);
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();

View file

@ -1,5 +1,5 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -34,14 +34,13 @@ public partial class CustomizationDrawer
private void DrawGenderSelector() private void DrawGenderSelector()
{ {
using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw)) using (ImRaii.Disabled(_locked || _lockedRedraw))
{ {
var icon = _customize.Gender switch var icon = _customize.Gender switch
{ {
Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble, Gender.Male => FontAwesomeIcon.Mars,
Gender.Male => FontAwesomeIcon.Mars, Gender.Female => FontAwesomeIcon.Venus,
Gender.Female => FontAwesomeIcon.Venus, _ => FontAwesomeIcon.Question,
_ => FontAwesomeIcon.Question,
}; };
if (ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, if (ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty,
@ -56,7 +55,7 @@ public partial class CustomizationDrawer
private void DrawRaceCombo() private void DrawRaceCombo()
{ {
using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw)) using (ImRaii.Disabled(_locked || _lockedRedraw))
{ {
ImGui.SetNextItemWidth(_raceSelectorWidth); ImGui.SetNextItemWidth(_raceSelectorWidth);
using (var combo = ImRaii.Combo("##subRaceCombo", _service.ClanName(_customize.Clan, _customize.Gender))) using (var combo = ImRaii.Combo("##subRaceCombo", _service.ClanName(_customize.Clan, _customize.Gender)))

View file

@ -1,7 +1,9 @@
using Glamourer.GameData; using Dalamud.Interface.Textures.TextureWraps;
using Glamourer.GameData;
using Glamourer.Unlocks; using Glamourer.Unlocks;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Extensions;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -29,10 +31,11 @@ public partial class CustomizationDrawer
npc = true; npc = true;
} }
var icon = _service.Manager.GetIcon(custom!.Value.IconId); var icon = _service.Manager.GetIcon(custom!.Value.IconId);
var hasIcon = icon.TryGetWrap(out var wrap, out _);
using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw))
{ {
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize))
{ {
ImGui.OpenPopup(IconSelectorPopup); ImGui.OpenPopup(IconSelectorPopup);
} }
@ -43,7 +46,8 @@ public partial class CustomizationDrawer
} }
} }
ImGuiUtil.HoverIconTooltip(icon, _iconSize); if (hasIcon)
ImGuiUtil.HoverIconTooltip(wrap!, _iconSize);
ImGui.SameLine(); ImGui.SameLine();
using (_ = ImRaii.Group()) using (_ = ImRaii.Group())
@ -83,8 +87,9 @@ public partial class CustomizationDrawer
using var frameColor = current == i using var frameColor = current == i
? ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed) ? ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed)
: ImRaii.PushColor(ImGuiCol.Button, ColorId.FavoriteStarOn.Value(), isFavorite); : ImRaii.PushColor(ImGuiCol.Button, ColorId.FavoriteStarOn.Value(), isFavorite);
var hasIcon = icon.TryGetWrap(out var wrap, out var _);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize))
{ {
UpdateValue(custom.Value); UpdateValue(custom.Value);
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
@ -96,8 +101,9 @@ public partial class CustomizationDrawer
else else
_favorites.TryAdd(_set.Gender, _set.Clan, _currentIndex, custom.Value); _favorites.TryAdd(_set.Gender, _set.Clan, _currentIndex, custom.Value);
ImGuiUtil.HoverIconTooltip(icon, _iconSize, if (hasIcon)
FavoriteManager.TypeAllowed(_currentIndex) ? "Right-Click to toggle favorite." : string.Empty); ImGuiUtil.HoverIconTooltip(wrap!, _iconSize,
FavoriteManager.TypeAllowed(_currentIndex) ? "Right-Click to toggle favorite." : string.Empty);
var text = custom.Value.ToString(); var text = custom.Value.ToString();
var textWidth = ImGui.CalcTextSize(text).X; var textWidth = ImGui.CalcTextSize(text).X;
@ -188,25 +194,36 @@ public partial class CustomizationDrawer
private void DrawMultiIcons() private void DrawMultiIcons()
{ {
var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark]; var options = _set.Order[MenuType.IconCheckmark];
using var group = ImRaii.Group(); using var group = ImRaii.Group();
var face = _set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0 ? _set.Faces[0].Value : _customize.Face; var face = _set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0 ? _set.Faces[0].Value : _customize.Face;
foreach (var (featureIdx, idx) in options.WithIndex()) foreach (var (featureIdx, idx) in options.WithIndex())
{ {
using var id = SetId(featureIdx); using var id = SetId(featureIdx);
var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero;
var feature = _set.Data(featureIdx, 0, face); var feature = _set.Data(featureIdx, 0, face);
var icon = featureIdx == CustomizeIndex.LegacyTattoo bool hasIcon;
? _legacyTattoo ?? _service.Manager.GetIcon(feature.IconId) IDalamudTextureWrap? wrap;
: _service.Manager.GetIcon(feature.IconId); var icon = _service.Manager.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, if (featureIdx is CustomizeIndex.LegacyTattoo)
Vector4.Zero, enabled ? Vector4.One : _redTint)) {
wrap = _legacyTattoo;
hasIcon = wrap != null;
}
else
{
hasIcon = icon.TryGetWrap(out wrap, out _);
}
if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize, Vector2.Zero, Vector2.One,
(int)ImGui.GetStyle().FramePadding.X, Vector4.Zero, enabled ? Vector4.One : _redTint))
{ {
_customize.Set(featureIdx, enabled ? CustomizeValue.Zero : CustomizeValue.Max); _customize.Set(featureIdx, enabled ? CustomizeValue.Zero : CustomizeValue.Max);
Changed |= _currentFlag; Changed |= _currentFlag;
} }
ImGuiUtil.HoverIconTooltip(icon, _iconSize); if (hasIcon)
ImGuiUtil.HoverIconTooltip(wrap!, _iconSize);
if (idx % 4 != 3) if (idx % 4 != 3)
ImGui.SameLine(); ImGui.SameLine();
} }

View file

@ -1,4 +1,4 @@
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGuiInternal; using OtterGuiInternal;
@ -29,6 +29,29 @@ public partial class CustomizationDrawer
ImGui.SameLine(); ImGui.SameLine();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(_currentOption); ImGui.TextUnformatted(_currentOption);
if (_currentIndex is CustomizeIndex.Height)
DrawHeight();
}
private void DrawHeight()
{
if (_config.HeightDisplayType is HeightDisplayType.None)
return;
var height = _heightService.Height(_customize);
ImGui.SameLine();
var heightString = _config.HeightDisplayType switch
{
HeightDisplayType.Centimetre => FormattableString.Invariant($"({height * 100:F1} cm)"),
HeightDisplayType.Metre => FormattableString.Invariant($"({height:F2} m)"),
HeightDisplayType.Wrong => FormattableString.Invariant($"({height * 100 / 2.539:F1} in)"),
HeightDisplayType.WrongFoot => $"({(int)(height * 100 / 2.539 / 12)}'{(int)(height * 100 / 2.539) % 12}'')",
HeightDisplayType.Corgi => FormattableString.Invariant($"({height * 100 / 40.0:F1} Corgis)"),
HeightDisplayType.OlympicPool => FormattableString.Invariant($"({height / 3.0:F3} Pools)"),
_ => FormattableString.Invariant($"({height})"),
};
ImGui.TextUnformatted(heightString);
} }
private void DrawPercentageSlider() private void DrawPercentageSlider()
@ -272,7 +295,7 @@ public partial class CustomizationDrawer
private void ApplyCheckbox(CustomizeIndex index) private void ApplyCheckbox(CustomizeIndex index)
{ {
SetId(index); using var id = SetId(index);
if (UiHelpers.DrawCheckbox("##apply", $"Apply the {_currentOption} customization in this design.", _currentApply, out _, _locked)) if (UiHelpers.DrawCheckbox("##apply", $"Apply the {_currentOption} customization in this design.", _currentApply, out _, _locked))
ToggleApply(); ToggleApply();
} }

View file

@ -1,10 +1,10 @@
using Dalamud.Interface.Internal; using Dalamud.Interface.Textures;
using Dalamud.Interface.Utility; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin; using Dalamud.Plugin.Services;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.Unlocks; using Glamourer.Unlocks;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -12,16 +12,21 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization; namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeService _service, CodeService _codes, Configuration _config, FavoriteManager _favorites) public partial class CustomizationDrawer(
ITextureProvider textures,
CustomizeService _service,
Configuration _config,
FavoriteManager _favorites,
HeightService _heightService)
: IDisposable : IDisposable
{ {
private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f);
private readonly IDalamudTextureWrap? _legacyTattoo = GetLegacyTattooIcon(pi); private readonly IDalamudTextureWrap? _legacyTattoo = GetLegacyTattooIcon(textures);
private Exception? _terminate; private Exception? _terminate;
private CustomizeArray _customize = CustomizeArray.Default; private CustomizeArray _customize = CustomizeArray.Default;
private CustomizeSet _set = null!; private CustomizeSet _set = null!;
public CustomizeArray Customize public CustomizeArray Customize
=> _customize; => _customize;
@ -46,7 +51,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeSer
public bool Draw(CustomizeArray current, bool locked, bool lockedRedraw) public bool Draw(CustomizeArray current, bool locked, bool lockedRedraw)
{ {
_withApply = false; _withApply = false;
Init(current, locked, lockedRedraw); Init(current, locked, lockedRedraw);
return DrawInternal(); return DrawInternal();
@ -111,30 +116,27 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeSer
try try
{ {
if (_codes.Enabled(CodeService.CodeFlag.Artisan))
return DrawArtisan();
DrawRaceGenderSelector(); DrawRaceGenderSelector();
DrawBodyType(); DrawBodyType();
_set = _service.Manager.GetSet(_customize.Clan, _customize.Gender); _set = _service.Manager.GetSet(_customize.Clan, _customize.Gender);
foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage]) foreach (var id in _set.Order[MenuType.Percentage])
PercentageSelector(id); PercentageSelector(id);
Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.IconSelector], DrawIconSelector, ImGui.SameLine); Functions.IteratePairwise(_set.Order[MenuType.IconSelector], DrawIconSelector, ImGui.SameLine);
DrawMultiIconSelector(); DrawMultiIconSelector();
foreach (var id in _set.Order[CharaMakeParams.MenuType.ListSelector]) foreach (var id in _set.Order[MenuType.ListSelector])
DrawListSelector(id, false); DrawListSelector(id, false);
foreach (var id in _set.Order[CharaMakeParams.MenuType.List1Selector]) foreach (var id in _set.Order[MenuType.List1Selector])
DrawListSelector(id, true); DrawListSelector(id, true);
Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine); Functions.IteratePairwise(_set.Order[MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine);
Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.Checkmark], DrawCheckbox, Functions.IteratePairwise(_set.Order[MenuType.Checkmark], DrawCheckbox,
() => ImGui.SameLine(_comboSelectorSize - _framedIconSize.X + ImGui.GetStyle().WindowPadding.X)); () => ImGui.SameLine(_comboSelectorSize - _framedIconSize.X + ImGui.GetStyle().WindowPadding.X));
return Changed != 0 || ChangeApply != _initialApply; return Changed != 0 || ChangeApply != _initialApply;
} }
@ -148,31 +150,6 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeSer
} }
} }
private unsafe bool DrawArtisan()
{
for (var i = 0; i < CustomizeArray.Size; ++i)
{
using var id = ImRaii.PushId(i);
int value = _customize.Data[i];
ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt(string.Empty, ref value, 0, 0))
{
var newValue = (byte)Math.Clamp(value, 0, byte.MaxValue);
if (newValue != _customize.Data[i])
foreach (var flag in Enum.GetValues<CustomizeIndex>())
{
var (j, _) = flag.ToByteAndMask();
if (j == i)
Changed |= flag.ToFlag();
}
_customize.Data[i] = newValue;
}
}
return Changed != 0;
}
private void UpdateSizes() private void UpdateSizes()
{ {
_spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }; _spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X };
@ -184,7 +161,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeSer
_raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X;
} }
private static IDalamudTextureWrap? GetLegacyTattooIcon(DalamudPluginInterface pi) private static IDalamudTextureWrap? GetLegacyTattooIcon(ITextureProvider textures)
{ {
using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw"); using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw");
if (resource == null) if (resource == null)
@ -193,7 +170,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeSer
var rawImage = new byte[resource.Length]; var rawImage = new byte[resource.Length];
var length = resource.Read(rawImage, 0, (int)resource.Length); var length = resource.Read(rawImage, 0, (int)resource.Length);
return length == resource.Length return length == resource.Length
? pi.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4) ? textures.CreateFromRaw(RawImageSpecification.Rgba32(192, 192), rawImage, "Glamourer.LegacyTattoo")
: null; : null;
} }
} }

View file

@ -6,8 +6,8 @@ namespace Glamourer.Gui.Customization;
public struct CustomizeParameterDrawData(CustomizeParameterFlag flag, in DesignData data) public struct CustomizeParameterDrawData(CustomizeParameterFlag flag, in DesignData data)
{ {
private IDesignEditor _editor; private IDesignEditor _editor = null!;
private object _object; private object _object = null!;
public readonly CustomizeParameterFlag Flag = flag; public readonly CustomizeParameterFlag Flag = flag;
public bool Locked; public bool Locked;
public bool DisplayApplication; public bool DisplayApplication;

View file

@ -3,7 +3,7 @@ using Glamourer.Designs;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Interop.PalettePlus; using Glamourer.Interop.PalettePlus;
using Glamourer.State; using Glamourer.State;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
@ -287,13 +287,13 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import
} }
private ImGuiColorEditFlags GetFlags() private ImGuiColorEditFlags GetFlags()
=> Format | Display | ImGuiColorEditFlags.HDR | ImGuiColorEditFlags.NoOptions; => Format | Display | ImGuiColorEditFlags.Hdr | ImGuiColorEditFlags.NoOptions;
private ImGuiColorEditFlags Format private ImGuiColorEditFlags Format
=> config.UseFloatForColors ? ImGuiColorEditFlags.Float : ImGuiColorEditFlags.Uint8; => config.UseFloatForColors ? ImGuiColorEditFlags.Float : ImGuiColorEditFlags.Uint8;
private ImGuiColorEditFlags Display private ImGuiColorEditFlags Display
=> config.UseRgbForColors ? ImGuiColorEditFlags.DisplayRGB : ImGuiColorEditFlags.DisplayHSV; => config.UseRgbForColors ? ImGuiColorEditFlags.DisplayRgb : ImGuiColorEditFlags.DisplayHsv;
private ImRaii.IEndObject EnsureSize() private ImRaii.IEndObject EnsureSize()
{ {

View file

@ -2,11 +2,13 @@
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Glamourer.Automation; using Glamourer.Automation;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Designs.Special; using Glamourer.Designs.Special;
using Glamourer.Events; using Glamourer.Events;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Log; using OtterGui.Log;
using OtterGui.Widgets; using OtterGui.Widgets;
@ -14,30 +16,31 @@ namespace Glamourer.Gui;
public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, string>>, IDisposable public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, string>>, IDisposable
{ {
private readonly EphemeralConfig _config; protected readonly EphemeralConfig Config;
private readonly DesignChanged _designChanged; protected readonly DesignChanged DesignChanged;
private readonly DesignColors _designColors; protected readonly DesignColors DesignColors;
protected readonly TabSelected TabSelected; protected readonly TabSelected TabSelected;
protected float InnerWidth; protected float InnerWidth;
private IDesignStandIn? _currentDesign; private IDesignStandIn? _currentDesign;
private bool _isCurrentSelectionDirty;
protected DesignComboBase(Func<IReadOnlyList<Tuple<IDesignStandIn, string>>> generator, Logger log, DesignChanged designChanged, protected DesignComboBase(Func<IReadOnlyList<Tuple<IDesignStandIn, string>>> generator, Logger log, DesignChanged designChanged,
TabSelected tabSelected, EphemeralConfig config, DesignColors designColors) TabSelected tabSelected, EphemeralConfig config, DesignColors designColors)
: base(generator, MouseWheelType.Control, log) : base(generator, MouseWheelType.Control, log)
{ {
_designChanged = designChanged; DesignChanged = designChanged;
TabSelected = tabSelected; TabSelected = tabSelected;
_config = config; Config = config;
_designColors = designColors; DesignColors = designColors;
_designChanged.Subscribe(OnDesignChange, DesignChanged.Priority.DesignCombo); DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo);
} }
public bool Incognito public bool Incognito
=> _config.IncognitoMode; => Config.IncognitoMode;
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
_designChanged.Unsubscribe(OnDesignChange); DesignChanged.Unsubscribe(OnDesignChanged);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
@ -45,52 +48,51 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
{ {
var (design, path) = Items[globalIdx]; var (design, path) = Items[globalIdx];
bool ret; bool ret;
if (design is Design realDesign) switch (design)
{ {
using var color = ImRaii.PushColor(ImGuiCol.Text, _designColors.GetColor(realDesign)); case Design realDesign:
ret = base.DrawSelectable(globalIdx, selected);
if (path.Length > 0 && realDesign.Name != path)
{ {
var start = ImGui.GetItemRectMin(); using var color = ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(realDesign));
var pos = start.X + ImGui.CalcTextSize(realDesign.Name).X; ret = base.DrawSelectable(globalIdx, selected);
var maxSize = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X; DrawPath(path, realDesign);
var remainingSpace = maxSize - pos; return ret;
var requiredSize = ImGui.CalcTextSize(path).X + ImGui.GetStyle().ItemInnerSpacing.X;
var offset = remainingSpace - requiredSize;
if (ImGui.GetScrollMaxY() == 0)
offset -= ImGui.GetStyle().ItemInnerSpacing.X;
if (offset < ImGui.GetStyle().ItemSpacing.X)
ImGuiUtil.HoverTooltip(path);
else
ImGui.GetWindowDrawList().AddText(start with { X = pos + offset },
ImGui.GetColorU32(ImGuiCol.TextDisabled), path);
} }
case QuickSelectedDesign quickDesign:
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.NormalDesign.Value());
ret = base.DrawSelectable(globalIdx, selected);
DrawResolvedDesign(quickDesign);
return ret;
}
default: return base.DrawSelectable(globalIdx, selected);
} }
else
{
ret = base.DrawSelectable(globalIdx, selected);
}
return ret;
} }
protected override int UpdateCurrentSelected(int currentSelected) private static void DrawPath(string path, Design realDesign)
{ {
CurrentSelectionIdx = Items.IndexOf(p => _currentDesign == p.Item1); if (path.Length <= 0 || realDesign.Name == path)
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : null; return;
return CurrentSelectionIdx;
DrawRightAligned(realDesign.Name, path, ImGui.GetColorU32(ImGuiCol.TextDisabled));
}
private void DrawResolvedDesign(QuickSelectedDesign quickDesign)
{
var linkedDesign = quickDesign.CurrentDesign;
if (linkedDesign != null)
DrawRightAligned(quickDesign.ResolveName(false), linkedDesign.Name.Text, DesignColors.GetColor(linkedDesign));
else
DrawRightAligned(quickDesign.ResolveName(false), "[Nothing]", DesignColors.MissingColor);
} }
protected bool Draw(IDesignStandIn? currentDesign, string? label, float width) protected bool Draw(IDesignStandIn? currentDesign, string? label, float width)
{ {
_currentDesign = currentDesign; _currentDesign = currentDesign;
InnerWidth = 400 * ImGuiHelpers.GlobalScale; UpdateCurrentSelection();
InnerWidth = 400 * ImGuiHelpers.GlobalScale;
var name = label ?? "Select Design Here..."; var name = label ?? "Select Design Here...";
bool ret; bool ret;
using (_ = currentDesign != null ? ImRaii.PushColor(ImGuiCol.Text, _designColors.GetColor(currentDesign as Design)) : null) using (_ = currentDesign != null ? ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(currentDesign as Design)) : null)
{ {
ret = Draw("##design", name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) ret = Draw("##design", name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing())
&& CurrentSelection != null; && CurrentSelection != null;
@ -103,6 +105,8 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
ImGuiUtil.HoverTooltip("Control + Right-Click to move to design."); ImGuiUtil.HoverTooltip("Control + Right-Click to move to design.");
} }
QuickSelectedDesignTooltip(currentDesign);
_currentDesign = null; _currentDesign = null;
return ret; return ret;
} }
@ -119,37 +123,101 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
return filter.IsContained(path) || filter.IsContained(design.ResolveName(false)); return filter.IsContained(path) || filter.IsContained(design.ResolveName(false));
} }
private void OnDesignChange(DesignChanged.Type type, Design design, object? data = null) protected override void OnMouseWheel(string preview, ref int _2, int steps)
{ {
switch (type) if (!ReferenceEquals(_currentDesign, CurrentSelection?.Item1))
{ CurrentSelectionIdx = -1;
case DesignChanged.Type.Created:
case DesignChanged.Type.Renamed:
case DesignChanged.Type.ChangedColor:
case DesignChanged.Type.Deleted:
case DesignChanged.Type.QuickDesignBar:
var priorState = IsInitialized;
if (priorState)
Cleanup();
CurrentSelectionIdx = Items.IndexOf(s => ReferenceEquals(s.Item1, CurrentSelection?.Item1));
if (CurrentSelectionIdx >= 0)
{
CurrentSelection = Items[CurrentSelectionIdx];
}
else if (Items.Count > 0)
{
CurrentSelectionIdx = 0;
CurrentSelection = Items[0];
}
else
{
CurrentSelection = null;
}
if (!priorState) base.OnMouseWheel(preview, ref _2, steps);
Cleanup(); }
break;
private void UpdateCurrentSelection()
{
if (!_isCurrentSelectionDirty)
return;
var priorState = IsInitialized;
if (priorState)
Cleanup();
CurrentSelectionIdx = Items.IndexOf(s => ReferenceEquals(s.Item1, CurrentSelection?.Item1));
if (CurrentSelectionIdx >= 0)
{
UpdateSelection(Items[CurrentSelectionIdx]);
} }
else if (Items.Count > 0)
{
CurrentSelectionIdx = 0;
UpdateSelection(Items[0]);
}
else
{
UpdateSelection(null);
}
if (!priorState)
Cleanup();
_isCurrentSelectionDirty = false;
}
protected override int UpdateCurrentSelected(int currentSelected)
{
CurrentSelectionIdx = Items.IndexOf(p => _currentDesign == p.Item1);
UpdateSelection(CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : null);
return CurrentSelectionIdx;
}
private void OnDesignChanged(DesignChanged.Type type, Design? _1, ITransaction? _2 = null)
{
_isCurrentSelectionDirty = type switch
{
DesignChanged.Type.Created => true,
DesignChanged.Type.Renamed => true,
DesignChanged.Type.ChangedColor => true,
DesignChanged.Type.Deleted => true,
DesignChanged.Type.QuickDesignBar => true,
_ => _isCurrentSelectionDirty,
};
}
private void QuickSelectedDesignTooltip(IDesignStandIn? design)
{
if (!ImGui.IsItemHovered())
return;
if (design is not QuickSelectedDesign q)
return;
using var tt = ImRaii.Tooltip();
var linkedDesign = q.CurrentDesign;
if (linkedDesign != null)
{
ImGui.TextUnformatted("Currently resolving to ");
using var color = ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(linkedDesign));
ImGui.SameLine(0, 0);
ImGui.TextUnformatted(linkedDesign.Name.Text);
}
else
{
ImGui.TextUnformatted("No design selected in the Quick Design Bar.");
}
}
private static void DrawRightAligned(string leftText, string text, uint color)
{
var start = ImGui.GetItemRectMin();
var pos = start.X + ImGui.CalcTextSize(leftText).X;
var maxSize = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X;
var remainingSpace = maxSize - pos;
var requiredSize = ImGui.CalcTextSize(text).X + ImGui.GetStyle().ItemInnerSpacing.X;
var offset = remainingSpace - requiredSize;
if (ImGui.GetScrollMaxY() == 0)
offset -= ImGui.GetStyle().ItemInnerSpacing.X;
if (offset < ImGui.GetStyle().ItemSpacing.X)
ImGuiUtil.HoverTooltip(text);
else
ImGui.GetWindowDrawList().AddText(start with { X = pos + offset },
color, text);
} }
} }
@ -176,8 +244,7 @@ public abstract class DesignCombo : DesignComboBase
public sealed class QuickDesignCombo : DesignCombo public sealed class QuickDesignCombo : DesignCombo
{ {
public QuickDesignCombo(DesignManager designs, public QuickDesignCombo(DesignFileSystem fileSystem,
DesignFileSystem fileSystem,
Logger log, Logger log,
DesignChanged designChanged, DesignChanged designChanged,
TabSelected tabSelected, TabSelected tabSelected,
@ -185,16 +252,49 @@ public sealed class QuickDesignCombo : DesignCombo
DesignColors designColors) DesignColors designColors)
: base(log, designChanged, tabSelected, config, designColors, () => : base(log, designChanged, tabSelected, config, designColors, () =>
[ [
.. designs.Designs .. fileSystem
.Where(d => d.QuickDesign) .Where(kvp => kvp.Key.QuickDesign)
.Select(d => new Tuple<IDesignStandIn, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) .Select(kvp => new Tuple<IDesignStandIn, string>(kvp.Key, kvp.Value.FullName()))
.OrderBy(d => d.Item2), .OrderBy(d => d.Item2),
]) ])
=> AllowMouseWheel = MouseWheelType.Unmodified; {
if (config.SelectedQuickDesign != Guid.Empty)
{
CurrentSelectionIdx = Items.IndexOf(t => t.Item1 is Design d && d.Identifier == config.SelectedQuickDesign);
if (CurrentSelectionIdx >= 0)
CurrentSelection = Items[CurrentSelectionIdx];
else if (Items.Count > 0)
CurrentSelectionIdx = 0;
}
AllowMouseWheel = MouseWheelType.Unmodified;
SelectionChanged += OnSelectionChange;
}
private void OnSelectionChange(Tuple<IDesignStandIn, string>? old, Tuple<IDesignStandIn, string>? @new)
{
if (old == null)
{
if (@new?.Item1 is not Design d)
return;
Config.SelectedQuickDesign = d.Identifier;
Config.Save();
}
else if (@new?.Item1 is not Design d)
{
Config.SelectedQuickDesign = Guid.Empty;
Config.Save();
}
else if (!old.Item1.Equals(@new.Item1))
{
Config.SelectedQuickDesign = d.Identifier;
Config.Save();
}
}
} }
public sealed class LinkDesignCombo( public sealed class LinkDesignCombo(
DesignManager designs,
DesignFileSystem fileSystem, DesignFileSystem fileSystem,
Logger log, Logger log,
DesignChanged designChanged, DesignChanged designChanged,
@ -203,8 +303,8 @@ public sealed class LinkDesignCombo(
DesignColors designColors) DesignColors designColors)
: DesignCombo(log, designChanged, tabSelected, config, designColors, () => : DesignCombo(log, designChanged, tabSelected, config, designColors, () =>
[ [
.. designs.Designs .. fileSystem
.Select(d => new Tuple<IDesignStandIn, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) .Select(kvp => new Tuple<IDesignStandIn, string>(kvp.Key, kvp.Value.FullName()))
.OrderBy(d => d.Item2), .OrderBy(d => d.Item2),
]); ]);
@ -218,8 +318,8 @@ public sealed class RandomDesignCombo(
DesignColors designColors) DesignColors designColors)
: DesignCombo(log, designChanged, tabSelected, config, designColors, () => : DesignCombo(log, designChanged, tabSelected, config, designColors, () =>
[ [
.. designs.Designs .. fileSystem
.Select(d => new Tuple<IDesignStandIn, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) .Select(kvp => new Tuple<IDesignStandIn, string>(kvp.Key, kvp.Value.FullName()))
.OrderBy(d => d.Item2), .OrderBy(d => d.Item2),
]) ])
{ {
@ -245,7 +345,6 @@ public sealed class RandomDesignCombo(
} }
public sealed class SpecialDesignCombo( public sealed class SpecialDesignCombo(
DesignManager designs,
DesignFileSystem fileSystem, DesignFileSystem fileSystem,
TabSelected tabSelected, TabSelected tabSelected,
DesignColors designColors, DesignColors designColors,
@ -253,11 +352,13 @@ public sealed class SpecialDesignCombo(
DesignChanged designChanged, DesignChanged designChanged,
AutoDesignManager autoDesignManager, AutoDesignManager autoDesignManager,
EphemeralConfig config, EphemeralConfig config,
RandomDesignGenerator rng) RandomDesignGenerator rng,
: DesignComboBase(() => designs.Designs QuickSelectedDesign quickSelectedDesign)
.Select(d => new Tuple<IDesignStandIn, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) : DesignComboBase(() => fileSystem
.Select(kvp => new Tuple<IDesignStandIn, string>(kvp.Key, kvp.Value.FullName()))
.OrderBy(d => d.Item2) .OrderBy(d => d.Item2)
.Prepend(new Tuple<IDesignStandIn, string>(new RandomDesign(rng), string.Empty)) .Prepend(new Tuple<IDesignStandIn, string>(new RandomDesign(rng), string.Empty))
.Prepend(new Tuple<IDesignStandIn, string>(quickSelectedDesign, string.Empty))
.Prepend(new Tuple<IDesignStandIn, string>(new RevertDesign(), string.Empty)) .Prepend(new Tuple<IDesignStandIn, string>(new RevertDesign(), string.Empty))
.ToList(), log, designChanged, tabSelected, config, designColors) .ToList(), log, designChanged, tabSelected, config, designColors)
{ {

View file

@ -6,25 +6,28 @@ using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Glamourer.Automation; using Glamourer.Automation;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Interop; using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.State; using Glamourer.State;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Text;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
namespace Glamourer.Gui; namespace Glamourer.Gui;
[Flags] [Flags]
public enum QdbButtons public enum QdbButtons
{ {
ApplyDesign = 0x01, ApplyDesign = 0x01,
RevertAll = 0x02, RevertAll = 0x02,
RevertAutomation = 0x04, RevertAutomation = 0x04,
RevertAdvanced = 0x08, RevertAdvancedDyes = 0x08,
RevertEquip = 0x10, RevertEquip = 0x10,
RevertCustomize = 0x20, RevertCustomize = 0x20,
ReapplyAutomation = 0x40,
ResetSettings = 0x80,
RevertAdvancedCustomization = 0x100,
} }
public sealed class DesignQuickBar : Window, IDisposable public sealed class DesignQuickBar : Window, IDisposable
@ -34,19 +37,21 @@ public sealed class DesignQuickBar : Window, IDisposable
? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove ? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove
: ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing; : ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing;
private readonly Configuration _config; private readonly Configuration _config;
private readonly QuickDesignCombo _designCombo; private readonly QuickDesignCombo _designCombo;
private readonly StateManager _stateManager; private readonly StateManager _stateManager;
private readonly AutoDesignApplier _autoDesignApplier; private readonly AutoDesignApplier _autoDesignApplier;
private readonly ObjectManager _objects; private readonly ActorObjectManager _objects;
private readonly IKeyState _keyState; private readonly PenumbraService _penumbra;
private readonly ImRaii.Style _windowPadding = new(); private readonly IKeyState _keyState;
private readonly ImRaii.Color _windowColor = new(); private readonly ImRaii.Style _windowPadding = new();
private DateTime _keyboardToggle = DateTime.UnixEpoch; private readonly ImRaii.Color _windowColor = new();
private int _numButtons; private DateTime _keyboardToggle = DateTime.UnixEpoch;
private int _numButtons;
private readonly StringBuilder _tooltipBuilder = new(512);
public DesignQuickBar(Configuration config, QuickDesignCombo designCombo, StateManager stateManager, IKeyState keyState, public DesignQuickBar(Configuration config, QuickDesignCombo designCombo, StateManager stateManager, IKeyState keyState,
ObjectManager objects, AutoDesignApplier autoDesignApplier) ActorObjectManager objects, AutoDesignApplier autoDesignApplier, PenumbraService penumbra)
: base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking) : base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking)
{ {
_config = config; _config = config;
@ -55,9 +60,11 @@ public sealed class DesignQuickBar : Window, IDisposable
_keyState = keyState; _keyState = keyState;
_objects = objects; _objects = objects;
_autoDesignApplier = autoDesignApplier; _autoDesignApplier = autoDesignApplier;
_penumbra = penumbra;
IsOpen = _config.Ephemeral.ShowDesignQuickBar; IsOpen = _config.Ephemeral.ShowDesignQuickBar;
DisableWindowSounds = true; DisableWindowSounds = true;
Size = Vector2.Zero; Size = Vector2.Zero;
RespectCloseHotkey = false;
} }
public void Dispose() public void Dispose()
@ -69,6 +76,9 @@ public sealed class DesignQuickBar : Window, IDisposable
IsOpen = _config.Ephemeral.ShowDesignQuickBar && _config.QdbButtons != 0; IsOpen = _config.Ephemeral.ShowDesignQuickBar && _config.QdbButtons != 0;
} }
public override bool DrawConditions()
=> _objects.Player.Valid;
public override void PreDraw() public override void PreDraw()
{ {
Flags = GetFlags; Flags = GetFlags;
@ -99,8 +109,7 @@ public sealed class DesignQuickBar : Window, IDisposable
private void Draw(float width) private void Draw(float width)
{ {
_objects.Update(); using var group = ImUtf8.Group();
using var group = ImRaii.Group();
var spacing = ImGui.GetStyle().ItemInnerSpacing; var spacing = ImGui.GetStyle().ItemInnerSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
var buttonSize = new Vector2(ImGui.GetFrameHeight()); var buttonSize = new Vector2(ImGui.GetFrameHeight());
@ -117,7 +126,10 @@ public sealed class DesignQuickBar : Window, IDisposable
DrawRevertEquipButton(buttonSize); DrawRevertEquipButton(buttonSize);
DrawRevertCustomizeButton(buttonSize); DrawRevertCustomizeButton(buttonSize);
DrawRevertAdvancedCustomization(buttonSize); DrawRevertAdvancedCustomization(buttonSize);
DrawRevertAdvancedDyes(buttonSize);
DrawRevertAutomationButton(buttonSize); DrawRevertAutomationButton(buttonSize);
DrawReapplyAutomationButton(buttonSize);
DrawResetSettingsButton(buttonSize);
} }
private ActorIdentifier _playerIdentifier; private ActorIdentifier _playerIdentifier;
@ -130,7 +142,6 @@ public sealed class DesignQuickBar : Window, IDisposable
private void PrepareButtons() private void PrepareButtons()
{ {
_objects.Update();
(_playerIdentifier, _playerData) = _objects.PlayerData; (_playerIdentifier, _playerData) = _objects.PlayerData;
(_targetIdentifier, _targetData) = _objects.TargetData; (_targetIdentifier, _targetData) = _objects.TargetData;
_playerState = _stateManager.GetValueOrDefault(_playerIdentifier); _playerState = _stateManager.GetValueOrDefault(_playerIdentifier);
@ -141,33 +152,38 @@ public sealed class DesignQuickBar : Window, IDisposable
{ {
var design = _designCombo.Design as Design; var design = _designCombo.Design as Design;
var available = 0; var available = 0;
var tooltip = string.Empty; _tooltipBuilder.Clear();
if (design == null) if (design == null)
{ {
tooltip = "No design selected."; _tooltipBuilder.Append("No design selected.");
} }
else else
{ {
if (_playerIdentifier.IsValid && _playerData.Valid) if (_playerIdentifier.IsValid && _playerData.Valid)
{ {
available |= 1; available |= 1;
tooltip = $"Left-Click: Apply {design.ResolveName(_config.Ephemeral.IncognitoMode)} to yourself."; _tooltipBuilder.Append("Left-Click: Apply ")
.Append(design.ResolveName(_config.Ephemeral.IncognitoMode))
.Append(" to yourself.");
} }
if (_targetIdentifier.IsValid && _targetData.Valid) if (_targetIdentifier.IsValid && _targetData.Valid)
{ {
if (available != 0) if (available != 0)
tooltip += '\n'; _tooltipBuilder.Append('\n');
available |= 2; available |= 2;
tooltip += $"Right-Click: Apply {design.ResolveName(_config.Ephemeral.IncognitoMode)} to {_targetIdentifier}."; _tooltipBuilder.Append("Right-Click: Apply ")
.Append(design.ResolveName(_config.Ephemeral.IncognitoMode))
.Append(" to ").Append(_config.Ephemeral.IncognitoMode ? _targetIdentifier.Incognito(null) : _targetIdentifier.ToName());
} }
if (available == 0) if (available == 0)
tooltip = "Neither player character nor target available."; _tooltipBuilder.Append("Neither player character nor target available.");
} }
var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.PlayCircle, size, tooltip, available); var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.PlayCircle, size, available);
ImGui.SameLine(); ImGui.SameLine();
if (!clicked) if (!clicked)
return; return;
@ -179,9 +195,8 @@ public sealed class DesignQuickBar : Window, IDisposable
return; return;
} }
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys());
using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters); _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true });
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks);
} }
private void DrawRevertButton(Vector2 buttonSize) private void DrawRevertButton(Vector2 buttonSize)
@ -190,28 +205,32 @@ public sealed class DesignQuickBar : Window, IDisposable
return; return;
var available = 0; var available = 0;
var tooltip = string.Empty; _tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false }) if (_playerIdentifier.IsValid && _playerState is { IsLocked: false })
{ {
available |= 1; available |= 1;
tooltip = "Left-Click: Revert the player character to their game state."; _tooltipBuilder.Append("Left-Click: Revert the player character to their game state.");
} }
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false }) if (_targetIdentifier.IsValid && _targetState is { IsLocked: false })
{ {
if (available != 0) if (available != 0)
tooltip += '\n'; _tooltipBuilder.Append('\n');
available |= 2; available |= 2;
tooltip += $"Right-Click: Revert {_targetIdentifier} to their game state."; _tooltipBuilder.Append("Right-Click: Revert ")
.Append(_targetIdentifier)
.Append(" to their game state.");
} }
if (available == 0) if (available == 0)
tooltip = "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."; _tooltipBuilder.Append(
"Neither player character nor target are available, have state modified by Glamourer, or their state is locked.");
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, tooltip, available); var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, available);
ImGui.SameLine(); ImGui.SameLine();
if (clicked) if (clicked)
_stateManager.ResetState(state!, StateSource.Manual); _stateManager.ResetState(state!, StateSource.Manual, isFinal: true);
} }
private void DrawRevertAutomationButton(Vector2 buttonSize) private void DrawRevertAutomationButton(Vector2 buttonSize)
@ -223,69 +242,147 @@ public sealed class DesignQuickBar : Window, IDisposable
return; return;
var available = 0; var available = 0;
var tooltip = string.Empty; _tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{ {
available |= 1; available |= 1;
tooltip = "Left-Click: Revert the player character to their automation state."; _tooltipBuilder.Append("Left-Click: Revert the player character to their automation state.");
} }
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid)
{ {
if (available != 0) if (available != 0)
tooltip += '\n'; _tooltipBuilder.Append('\n');
available |= 2; available |= 2;
tooltip += $"Right-Click: Revert {_targetIdentifier} to their automation state."; _tooltipBuilder.Append("Right-Click: Revert ")
.Append(_targetIdentifier)
.Append(" to their automation state.");
} }
if (available == 0) if (available == 0)
tooltip = "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."; _tooltipBuilder.Append(
"Neither player character nor target are available, have state modified by Glamourer, or their state is locked.");
var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.SyncAlt, buttonSize, tooltip, available); var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.SyncAlt, buttonSize, available);
ImGui.SameLine(); ImGui.SameLine();
if (!clicked) if (!clicked)
return; return;
foreach (var actor in data.Objects) foreach (var actor in data.Objects)
{ {
_autoDesignApplier.ReapplyAutomation(actor, id, state!); _autoDesignApplier.ReapplyAutomation(actor, id, state!, true, false, out var forcedRedraw);
_stateManager.ReapplyState(actor, StateSource.Manual); _stateManager.ReapplyAutomationState(actor, forcedRedraw, true, StateSource.Manual);
} }
} }
private void DrawRevertAdvancedCustomization(Vector2 buttonSize) private void DrawReapplyAutomationButton(Vector2 buttonSize)
{ {
if (!_config.UseAdvancedParameters) if (!_config.EnableAutoDesigns)
return; return;
if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvanced)) if (!_config.QdbButtons.HasFlag(QdbButtons.ReapplyAutomation))
return; return;
var available = 0; var available = 0;
var tooltip = string.Empty; _tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{ {
available |= 1; available |= 1;
tooltip = "Left-Click: Revert the advanced customizations and dyes of the player character to their game state."; _tooltipBuilder.Append("Left-Click: Reapply the player character's current automation on top of their current state.");
} }
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid)
{ {
if (available != 0) if (available != 0)
tooltip += '\n'; _tooltipBuilder.Append('\n');
available |= 2; available |= 2;
tooltip += $"Right-Click: Revert the advanced customizations and dyes of {_targetIdentifier} to their game state."; _tooltipBuilder.Append("Right-Click: Reapply ")
.Append(_targetIdentifier)
.Append("'s current automation on top of their current state.");
} }
if (available == 0) if (available == 0)
tooltip = "Neither player character nor target are available or their state is locked."; _tooltipBuilder.Append(
"Neither player character nor target are available, have state modified by Glamourer, or their state is locked.");
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Palette, buttonSize, tooltip, available); var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.Repeat, buttonSize, available);
ImGui.SameLine();
if (!clicked)
return;
foreach (var actor in data.Objects)
{
_autoDesignApplier.ReapplyAutomation(actor, id, state!, false, false, out var forcedRedraw);
_stateManager.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Manual);
}
}
private void DrawRevertAdvancedCustomization(Vector2 buttonSize)
{
if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization))
return;
var available = 0;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
available |= 1;
_tooltipBuilder.Append("Left-Click: Revert the advanced customizations of the player character to their game state.");
}
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid)
{
if (available != 0)
_tooltipBuilder.Append('\n');
available |= 2;
_tooltipBuilder.Append("Right-Click: Revert the advanced customizations of ")
.Append(_targetIdentifier)
.Append(" to their game state.");
}
if (available == 0)
_tooltipBuilder.Append("Neither player character nor target are available or their state is locked.");
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.PaintBrush, buttonSize, available);
ImGui.SameLine(); ImGui.SameLine();
if (clicked) if (clicked)
_stateManager.ResetAdvancedState(state!, StateSource.Manual); _stateManager.ResetAdvancedCustomizations(state!, StateSource.Manual);
}
private void DrawRevertAdvancedDyes(Vector2 buttonSize)
{
if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes))
return;
var available = 0;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
available |= 1;
_tooltipBuilder.Append("Left-Click: Revert the advanced dyes of the player character to their game state.");
}
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid)
{
if (available != 0)
_tooltipBuilder.Append('\n');
available |= 2;
_tooltipBuilder.Append("Right-Click: Revert the advanced dyes of ")
.Append(_targetIdentifier)
.Append(" to their game state.");
}
if (available == 0)
_tooltipBuilder.Append("Neither player character nor target are available or their state is locked.");
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Palette, buttonSize, available);
ImGui.SameLine();
if (clicked)
_stateManager.ResetAdvancedDyes(state!, StateSource.Manual);
} }
private void DrawRevertCustomizeButton(Vector2 buttonSize) private void DrawRevertCustomizeButton(Vector2 buttonSize)
@ -294,26 +391,28 @@ public sealed class DesignQuickBar : Window, IDisposable
return; return;
var available = 0; var available = 0;
var tooltip = string.Empty; _tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{ {
available |= 1; available |= 1;
tooltip = "Left-Click: Revert the customizations of the player character to their game state."; _tooltipBuilder.Append("Left-Click: Revert the customizations of the player character to their game state.");
} }
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid)
{ {
if (available != 0) if (available != 0)
tooltip += '\n'; _tooltipBuilder.Append('\n');
available |= 2; available |= 2;
tooltip += $"Right-Click: Revert the customizations of {_targetIdentifier} to their game state."; _tooltipBuilder.Append("Right-Click: Revert the customizations of ")
.Append(_targetIdentifier)
.Append(" to their game state.");
} }
if (available == 0) if (available == 0)
tooltip = "Neither player character nor target are available or their state is locked."; _tooltipBuilder.Append("Neither player character nor target are available or their state is locked.");
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.User, buttonSize, tooltip, available); var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.User, buttonSize, available);
ImGui.SameLine(); ImGui.SameLine();
if (clicked) if (clicked)
_stateManager.ResetCustomize(state!, StateSource.Manual); _stateManager.ResetCustomize(state!, StateSource.Manual);
@ -325,35 +424,80 @@ public sealed class DesignQuickBar : Window, IDisposable
return; return;
var available = 0; var available = 0;
var tooltip = string.Empty; _tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{ {
available |= 1; available |= 1;
tooltip = "Left-Click: Revert the equipment of the player character to its game state."; _tooltipBuilder.Append("Left-Click: Revert the equipment of the player character to its game state.");
} }
if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid)
{ {
if (available != 0) if (available != 0)
tooltip += '\n'; _tooltipBuilder.Append('\n');
available |= 2; available |= 2;
tooltip += $"Right-Click: Revert the equipment of {_targetIdentifier} to its game state."; _tooltipBuilder.Append("Right-Click: Revert the equipment of ")
.Append(_targetIdentifier)
.Append(" to its game state.");
} }
if (available == 0) if (available == 0)
tooltip = "Neither player character nor target are available or their state is locked."; _tooltipBuilder.Append("Neither player character nor target are available or their state is locked.");
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Vest, buttonSize, tooltip, available); var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Vest, buttonSize, available);
ImGui.SameLine(); ImGui.SameLine();
if (clicked) if (clicked)
_stateManager.ResetEquip(state!, StateSource.Manual); _stateManager.ResetEquip(state!, StateSource.Manual);
} }
private (bool, ActorIdentifier, ActorData, ActorState?) ResolveTarget(FontAwesomeIcon icon, Vector2 buttonSize, string tooltip, private void DrawResetSettingsButton(Vector2 buttonSize)
int available)
{ {
ImGuiUtil.DrawDisabledButton(icon.ToIconString(), buttonSize, tooltip, available == 0, true); if (!_config.QdbButtons.HasFlag(QdbButtons.ResetSettings))
return;
var available = 0;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerData.Valid)
{
available |= 1;
_tooltipBuilder
.Append(
"Left-Click: Reset all temporary settings applied by Glamourer (manually or through automation) to the collection affecting ")
.Append(_playerIdentifier)
.Append('.');
}
if (_targetIdentifier.IsValid && _targetData.Valid)
{
if (available != 0)
_tooltipBuilder.Append('\n');
available |= 2;
_tooltipBuilder
.Append(
"Right-Click: Reset all temporary settings applied by Glamourer (manually or through automation) to the collection affecting ")
.Append(_targetIdentifier)
.Append('.');
}
if (available == 0)
_tooltipBuilder.Append("Neither player character nor target are available to identify their collections.");
var (clicked, _, data, _) = ResolveTarget(FontAwesomeIcon.Cog, buttonSize, available);
ImGui.SameLine();
if (clicked)
{
_penumbra.RemoveAllTemporarySettings(data.Objects[0].Index, StateSource.Manual);
_penumbra.RemoveAllTemporarySettings(data.Objects[0].Index, StateSource.Fixed);
}
}
private (bool, ActorIdentifier, ActorData, ActorState?) ResolveTarget(FontAwesomeIcon icon, Vector2 buttonSize, int available)
{
var enumerator = _tooltipBuilder.GetChunks();
var span = enumerator.MoveNext() ? enumerator.Current.Span : [];
ImUtf8.IconButton(icon, span, buttonSize, available == 0);
if ((available & 1) == 1 && ImGui.IsItemClicked(ImGuiMouseButton.Left)) if ((available & 1) == 1 && ImGui.IsItemClicked(ImGuiMouseButton.Left))
return (true, _playerIdentifier, _playerData, _playerState); return (true, _playerIdentifier, _playerData, _playerState);
if ((available & 2) == 2 && ImGui.IsItemClicked(ImGuiMouseButton.Right)) if ((available & 2) == 2 && ImGui.IsItemClicked(ImGuiMouseButton.Right))
@ -385,14 +529,24 @@ public sealed class DesignQuickBar : Window, IDisposable
_numButtons = 0; _numButtons = 0;
if (_config.QdbButtons.HasFlag(QdbButtons.RevertAll)) if (_config.QdbButtons.HasFlag(QdbButtons.RevertAll))
++_numButtons; ++_numButtons;
if (_config.EnableAutoDesigns && _config.QdbButtons.HasFlag(QdbButtons.RevertAutomation)) if (_config.EnableAutoDesigns)
{
if (_config.QdbButtons.HasFlag(QdbButtons.RevertAutomation))
++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.ReapplyAutomation))
++_numButtons;
}
if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization))
++_numButtons; ++_numButtons;
if ((_config.UseAdvancedParameters || _config.UseAdvancedDyes) && _config.QdbButtons.HasFlag(QdbButtons.RevertAdvanced)) if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes))
++_numButtons; ++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.RevertCustomize)) if (_config.QdbButtons.HasFlag(QdbButtons.RevertCustomize))
++_numButtons; ++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.RevertEquip)) if (_config.QdbButtons.HasFlag(QdbButtons.RevertEquip))
++_numButtons; ++_numButtons;
if (_config.UseTemporarySettings && _config.QdbButtons.HasFlag(QdbButtons.ResetSettings))
++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.ApplyDesign)) if (_config.QdbButtons.HasFlag(QdbButtons.ApplyDesign))
{ {
++_numButtons; ++_numButtons;

View file

@ -0,0 +1,61 @@
using Glamourer.Designs;
using Glamourer.Interop.Material;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
public struct BonusDrawData(BonusItemFlag slot, in DesignData designData)
{
private IDesignEditor _editor = null!;
private object _object = null!;
public readonly BonusItemFlag Slot = slot;
public bool Locked;
public bool DisplayApplication;
public bool AllowRevert;
public bool HasAdvancedDyes;
public readonly bool IsDesign
=> _object is Design;
public readonly bool IsState
=> _object is ActorState;
public readonly void SetItem(EquipItem item)
=> _editor.ChangeBonusItem(_object, Slot, item, ApplySettings.Manual);
public readonly void SetApplyItem(bool value)
{
var manager = (DesignManager)_editor;
var design = (Design)_object;
manager.ChangeApplyBonusItem(design, Slot, value);
}
public EquipItem CurrentItem = designData.BonusItem(slot);
public EquipItem GameItem = default;
public bool CurrentApply;
public static BonusDrawData FromDesign(DesignManager manager, Design design, BonusItemFlag slot)
=> new(slot, design.DesignData)
{
_editor = manager,
_object = design,
CurrentApply = design.DoApplyBonusItem(slot),
Locked = design.WriteProtected(),
DisplayApplication = true,
HasAdvancedDyes = design.GetMaterialDataRef().CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)),
};
public static BonusDrawData FromState(StateManager manager, ActorState state, BonusItemFlag slot)
=> new(slot, state.ModelData)
{
_editor = manager,
_object = state,
Locked = state.IsLocked,
DisplayApplication = false,
GameItem = state.BaseData.BonusItem(slot),
AllowRevert = true,
HasAdvancedDyes = state.Materials.CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)),
};
}

View file

@ -0,0 +1,121 @@
using Dalamud.Plugin.Services;
using Glamourer.Services;
using Glamourer.Unlocks;
using Dalamud.Bindings.ImGui;
using Lumina.Excel.Sheets;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
public sealed class BonusItemCombo : FilterComboCache<EquipItem>
{
private readonly FavoriteManager _favorites;
public readonly string Label;
private CustomItemId _currentItem;
private float _innerWidth;
public PrimaryId CustomSetId { get; private set; }
public Variant CustomVariant { get; private set; }
public BonusItemCombo(IDataManager gameData, ItemManager items, BonusItemFlag slot, Logger log, FavoriteManager favorites)
: base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log)
{
_favorites = favorites;
Label = GetLabel(gameData, slot);
_currentItem = 0;
SearchByParts = true;
}
protected override void DrawList(float width, float itemHeight)
{
base.DrawList(width, itemHeight);
if (NewSelection != null && Items.Count > NewSelection.Value)
CurrentSelection = Items[NewSelection.Value];
}
protected override int UpdateCurrentSelected(int currentSelected)
{
if (CurrentSelection.Id == _currentItem)
return currentSelected;
CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItem);
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
return base.UpdateCurrentSelected(CurrentSelectionIdx);
}
public bool Draw(string previewName, BonusItemId previewIdx, float width, float innerWidth)
{
_innerWidth = innerWidth;
_currentItem = previewIdx;
CustomVariant = 0;
return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
}
protected override float GetFilterWidth()
=> _innerWidth - 2 * ImGui.GetStyle().FramePadding.X;
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var obj = Items[globalIdx];
var name = ToString(obj);
if (UiHelpers.DrawFavoriteStar(_favorites, obj) && CurrentSelectionIdx == globalIdx)
{
CurrentSelectionIdx = -1;
_currentItem = obj.Id;
CurrentSelection = default;
}
ImGui.SameLine();
var ret = ImGui.Selectable(name, selected);
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
ImGuiUtil.RightAlign($"({obj.PrimaryId.Id}-{obj.Variant.Id})");
return ret;
}
protected override bool IsVisible(int globalIndex, LowerString filter)
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString());
protected override string ToString(EquipItem obj)
=> obj.Name;
private static string GetLabel(IDataManager gameData, BonusItemFlag slot)
{
var sheet = gameData.GetExcelSheet<Addon>()!;
return slot switch
{
BonusItemFlag.Glasses => sheet.TryGetRow(16050, out var text) ? text.Text.ToString() : "Facewear",
BonusItemFlag.UnkSlot => sheet.TryGetRow(16051, out var text) ? text.Text.ToString() : "Facewear",
_ => string.Empty,
};
}
private static List<EquipItem> GetItems(FavoriteManager favorites, ItemManager items, BonusItemFlag slot)
{
var nothing = EquipItem.BonusItemNothing(slot);
return items.ItemData.ByType[slot.ToEquipType()].OrderByDescending(favorites.Contains).ThenBy(i => i.Id.Id).Prepend(nothing).ToList();
}
protected override void OnClosePopup()
{
// If holding control while the popup closes, try to parse the input as a full pair of set id and variant, and set a custom item for that.
if (!ImGui.GetIO().KeyCtrl)
return;
var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (split.Length != 2 || !ushort.TryParse(split[0], out var setId) || !byte.TryParse(split[1], out var variant))
return;
CustomSetId = setId;
CustomVariant = variant;
}
}

View file

@ -1,4 +1,5 @@
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Interop.Material;
using Glamourer.State; using Glamourer.State;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -7,9 +8,9 @@ namespace Glamourer.Gui.Equipment;
public struct EquipDrawData(EquipSlot slot, in DesignData designData) public struct EquipDrawData(EquipSlot slot, in DesignData designData)
{ {
private IDesignEditor _editor; private IDesignEditor _editor = null!;
private object _object; private object _object = null!;
public readonly EquipSlot Slot = slot; public readonly EquipSlot Slot = slot;
public bool Locked; public bool Locked;
public bool DisplayApplication; public bool DisplayApplication;
public bool AllowRevert; public bool AllowRevert;
@ -23,29 +24,31 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
public readonly void SetItem(EquipItem item) public readonly void SetItem(EquipItem item)
=> _editor.ChangeItem(_object, Slot, item, ApplySettings.Manual); => _editor.ChangeItem(_object, Slot, item, ApplySettings.Manual);
public readonly void SetStain(StainId stain) public readonly void SetStains(StainIds stains)
=> _editor.ChangeStain(_object, Slot, stain, ApplySettings.Manual); => _editor.ChangeStains(_object, Slot, stains, ApplySettings.Manual);
public readonly void SetStain(int which, StainId stain)
=> _editor.ChangeStains(_object, Slot, CurrentStains.With(which, stain), ApplySettings.Manual);
public readonly void SetApplyItem(bool value) public readonly void SetApplyItem(bool value)
{ {
var manager = (DesignManager)_editor; var manager = (DesignManager)_editor;
var design = (Design)_object; manager.ChangeApplyItem((Design)_object, Slot, value);
manager.ChangeApplyItem(design, Slot, value);
} }
public readonly void SetApplyStain(bool value) public readonly void SetApplyStain(bool value)
{ {
var manager = (DesignManager)_editor; var manager = (DesignManager)_editor;
var design = (Design)_object; manager.ChangeApplyStains((Design)_object, Slot, value);
manager.ChangeApplyStain(design, Slot, value);
} }
public EquipItem CurrentItem = designData.Item(slot); public EquipItem CurrentItem = designData.Item(slot);
public StainId CurrentStain = designData.Stain(slot); public StainIds CurrentStains = designData.Stain(slot);
public EquipItem GameItem = default; public EquipItem GameItem = default;
public StainId GameStain = default; public StainIds GameStains = default;
public bool CurrentApply; public bool CurrentApply;
public bool CurrentApplyStain; public bool CurrentApplyStain;
public bool HasAdvancedDyes;
public readonly Gender CurrentGender = designData.Customize.Gender; public readonly Gender CurrentGender = designData.Customize.Gender;
public readonly Race CurrentRace = designData.Customize.Race; public readonly Race CurrentRace = designData.Customize.Race;
@ -58,6 +61,7 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
CurrentApply = design.DoApplyEquip(slot), CurrentApply = design.DoApplyEquip(slot),
CurrentApplyStain = design.DoApplyStain(slot), CurrentApplyStain = design.DoApplyStain(slot),
Locked = design.WriteProtected(), Locked = design.WriteProtected(),
HasAdvancedDyes = design.GetMaterialDataRef().CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)),
DisplayApplication = true, DisplayApplication = true,
}; };
@ -69,7 +73,8 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
Locked = state.IsLocked, Locked = state.IsLocked,
DisplayApplication = false, DisplayApplication = false,
GameItem = state.BaseData.Item(slot), GameItem = state.BaseData.Item(slot),
GameStain = state.BaseData.Stain(slot), GameStains = state.BaseData.Stain(slot),
HasAdvancedDyes = state.Materials.CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)),
AllowRevert = true, AllowRevert = true,
}; };
} }

View file

@ -0,0 +1,83 @@
using Glamourer.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
[InlineArray(13)]
public struct EquipItemSlotCache
{
private EquipItem _element;
public EquipItem Dragged
{
get => this[^1];
set => this[^1] = value;
}
public void Clear()
=> ((Span<EquipItem>)this).Clear();
public EquipItem this[EquipSlot slot]
{
get => this[(int)slot.ToIndex()];
set => this[(int)slot.ToIndex()] = value;
}
public void Update(ItemManager items, in EquipItem item, EquipSlot startSlot)
{
if (item.Id == Dragged.Id && item.Type == Dragged.Type)
return;
switch (startSlot)
{
case EquipSlot.MainHand:
{
Clear();
this[EquipSlot.MainHand] = item;
if (item.Type is FullEquipType.Sword)
this[EquipSlot.OffHand] = items.FindClosestShield(item.ItemId, out var shield) ? shield : default;
else
this[EquipSlot.OffHand] = items.ItemData.Secondary.GetValueOrDefault(item.ItemId);
break;
}
case EquipSlot.OffHand:
{
Clear();
if (item.Type is FullEquipType.Shield)
this[EquipSlot.MainHand] = items.FindClosestSword(item.ItemId, out var sword) ? sword : default;
else
this[EquipSlot.MainHand] = items.ItemData.Primary.GetValueOrDefault(item.ItemId);
this[EquipSlot.OffHand] = item;
break;
}
default:
{
this[EquipSlot.MainHand] = default;
this[EquipSlot.OffHand] = default;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
if (startSlot == slot)
{
this[slot] = item;
continue;
}
var slotItem = items.Identify(slot, item.PrimaryId, item.Variant);
if (!slotItem.Valid || slotItem.ItemId.Id is not 0 != item.ItemId.Id is not 0)
{
slotItem = items.Identify(EquipSlot.OffHand, item.PrimaryId, item.SecondaryId, 1, item.Type);
if (slotItem.ItemId.Id is not 0 != item.ItemId.Id is not 0)
slotItem = default;
}
this[slot] = slotItem;
}
break;
}
}
Dragged = item;
}
}

View file

@ -5,10 +5,13 @@ using Glamourer.Events;
using Glamourer.Gui.Materials; using Glamourer.Gui.Materials;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.Unlocks; using Glamourer.Unlocks;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui.Extensions;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -23,42 +26,49 @@ public class EquipmentDrawer
private readonly GlamourerColorCombo _stainCombo; private readonly GlamourerColorCombo _stainCombo;
private readonly DictStain _stainData; private readonly DictStain _stainData;
private readonly ItemCombo[] _itemCombo; private readonly ItemCombo[] _itemCombo;
private readonly BonusItemCombo[] _bonusItemCombo;
private readonly Dictionary<FullEquipType, WeaponCombo> _weaponCombo; private readonly Dictionary<FullEquipType, WeaponCombo> _weaponCombo;
private readonly CodeService _codes;
private readonly TextureService _textures; private readonly TextureService _textures;
private readonly Configuration _config; private readonly Configuration _config;
private readonly GPoseService _gPose; private readonly GPoseService _gPose;
private readonly AdvancedDyePopup _advancedDyes; private readonly AdvancedDyePopup _advancedDyes;
private readonly ItemCopyService _itemCopy;
private float _requiredComboWidthUnscaled; private float _requiredComboWidthUnscaled;
private float _requiredComboWidth; private float _requiredComboWidth;
public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, CodeService codes, TextureService textures, private Stain? _draggedStain;
Configuration config, GPoseService gPose, AdvancedDyePopup advancedDyes) private EquipItemSlotCache _draggedItem;
private EquipSlot _dragTarget;
public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, TextureService textures,
Configuration config, GPoseService gPose, AdvancedDyePopup advancedDyes, ItemCopyService itemCopy)
{ {
_items = items; _items = items;
_codes = codes; _textures = textures;
_textures = textures; _config = config;
_config = config; _gPose = gPose;
_gPose = gPose; _advancedDyes = advancedDyes;
_advancedDyes = advancedDyes; _itemCopy = itemCopy;
_stainData = items.Stains; _stainData = items.Stains;
_stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites); _stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites);
_itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray(); _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray();
_weaponCombo = new Dictionary<FullEquipType, WeaponCombo>(FullEquipTypeExtensions.WeaponTypes.Count * 2); _bonusItemCombo = BonusExtensions.AllFlags.Select(f => new BonusItemCombo(gameData, items, f, Glamourer.Log, favorites)).ToArray();
_weaponCombo = new Dictionary<FullEquipType, WeaponCombo>(FullEquipTypeExtensions.WeaponTypes.Count * 2);
foreach (var type in Enum.GetValues<FullEquipType>()) foreach (var type in Enum.GetValues<FullEquipType>())
{ {
if (type.ToSlot() is EquipSlot.MainHand) if (type.ToSlot() is EquipSlot.MainHand)
_weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log)); _weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log, favorites));
else if (type.ToSlot() is EquipSlot.OffHand) else if (type.ToSlot() is EquipSlot.OffHand)
_weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log)); _weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log, favorites));
} }
_weaponCombo.Add(FullEquipType.Unknown, new WeaponCombo(items, FullEquipType.Unknown, Glamourer.Log)); _weaponCombo.Add(FullEquipType.Unknown, new WeaponCombo(items, FullEquipType.Unknown, Glamourer.Log, favorites));
} }
private Vector2 _iconSize; private Vector2 _iconSize;
private float _comboLength; private float _comboLength;
private uint _advancedMaterialColor;
public void Prepare() public void Prepare()
{ {
@ -70,7 +80,9 @@ public class EquipmentDrawer
.Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X) .Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X)
/ ImGuiHelpers.GlobalScale; / ImGuiHelpers.GlobalScale;
_requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale; _requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale;
_advancedMaterialColor = ColorId.AdvancedDyeActive.Value();
_dragTarget = EquipSlot.Unknown;
} }
private bool VerifyRestrictedGear(EquipDrawData data) private bool VerifyRestrictedGear(EquipDrawData data)
@ -87,21 +99,34 @@ public class EquipmentDrawer
if (_config.HideApplyCheckmarks) if (_config.HideApplyCheckmarks)
equipDrawData.DisplayApplication = false; equipDrawData.DisplayApplication = false;
using var id = ImRaii.PushId((int)equipDrawData.Slot); using var id = ImUtf8.PushId((int)equipDrawData.Slot);
var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y };
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
if (_config.SmallEquip) if (_config.SmallEquip)
DrawEquipSmall(equipDrawData); DrawEquipSmall(equipDrawData);
else if (!equipDrawData.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan))
DrawEquipArtisan(equipDrawData);
else else
DrawEquipNormal(equipDrawData); DrawEquipNormal(equipDrawData);
} }
public void DrawBonusItem(BonusDrawData bonusDrawData)
{
if (_config.HideApplyCheckmarks)
bonusDrawData.DisplayApplication = false;
using var id = ImUtf8.PushId(100 + (int)bonusDrawData.Slot);
var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y };
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
if (_config.SmallEquip)
DrawBonusItemSmall(bonusDrawData);
else
DrawBonusItemNormal(bonusDrawData);
}
public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons)
{ {
if (mainhand.CurrentItem.PrimaryId.Id == 0) if (mainhand.CurrentItem.PrimaryId.Id == 0 && !allWeapons)
return; return;
if (_config.HideApplyCheckmarks) if (_config.HideApplyCheckmarks)
@ -110,14 +135,12 @@ public class EquipmentDrawer
offhand.DisplayApplication = false; offhand.DisplayApplication = false;
} }
using var id = ImRaii.PushId("Weapons"); using var id = ImUtf8.PushId("Weapons"u8);
var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y };
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
if (_config.SmallEquip) if (_config.SmallEquip)
DrawWeaponsSmall(mainhand, offhand, allWeapons); DrawWeaponsSmall(mainhand, offhand, allWeapons);
else if (!mainhand.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan))
DrawWeaponsArtisan(mainhand, offhand);
else else
DrawWeaponsNormal(mainhand, offhand, allWeapons); DrawWeaponsNormal(mainhand, offhand, allWeapons);
} }
@ -140,136 +163,31 @@ public class EquipmentDrawer
} }
} }
public bool DrawAllStain(out StainId ret, bool locked) public bool DrawAllStain(out StainIds ret, bool locked)
{ {
using var disabled = ImRaii.Disabled(locked); using var disabled = ImRaii.Disabled(locked);
var change = _stainCombo.Draw("Dye All Slots", Stain.None.RgbaColor, string.Empty, false, false, MouseWheelType.None); var change = _stainCombo.Draw("Dye All Slots", Stain.None.RgbaColor, string.Empty, false, false, MouseWheelType.None);
ret = Stain.None.RowIndex; ret = StainIds.None;
if (change) if (change)
if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out var stain)) if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out var stain))
ret = stain.RowIndex; ret = StainIds.All(stain.RowIndex);
else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex)
ret = Stain.None.RowIndex; ret = StainIds.None;
if (!locked) if (!locked)
{ {
if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _config.DeleteDesignModifier.IsActive()) if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _config.DeleteDesignModifier.IsActive())
{ {
ret = Stain.None.RowIndex; ret = StainIds.None;
change = true; change = true;
} }
ImGuiUtil.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear."); ImUtf8.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear.");
} }
return change; return change;
} }
#region Artisan
private void DrawEquipArtisan(EquipDrawData data)
{
DrawStainArtisan(data);
ImGui.SameLine();
DrawArmorArtisan(data);
if (!data.DisplayApplication)
return;
ImGui.SameLine();
DrawApply(data);
ImGui.SameLine();
DrawApplyStain(data);
}
private void DrawWeaponsArtisan(in EquipDrawData mainhand, in EquipDrawData offhand)
{
using (var _ = ImRaii.PushId(0))
{
DrawStainArtisan(mainhand);
ImGui.SameLine();
DrawWeapon(mainhand);
}
using (var _ = ImRaii.PushId(1))
{
DrawStainArtisan(offhand);
ImGui.SameLine();
DrawWeapon(offhand);
}
return;
void DrawWeapon(in EquipDrawData current)
{
int setId = current.CurrentItem.PrimaryId.Id;
int type = current.CurrentItem.SecondaryId.Id;
int variant = current.CurrentItem.Variant.Id;
ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##setId", ref setId, 0, 0))
{
var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue);
if (newSetId.Id != current.CurrentItem.PrimaryId.Id)
current.SetItem(_items.Identify(current.Slot, newSetId, current.CurrentItem.SecondaryId, current.CurrentItem.Variant));
}
ImGui.SameLine();
ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##type", ref type, 0, 0))
{
var newType = (SecondaryId)Math.Clamp(type, 0, ushort.MaxValue);
if (newType.Id != current.CurrentItem.SecondaryId.Id)
current.SetItem(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, newType, current.CurrentItem.Variant));
}
ImGui.SameLine();
ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##variant", ref variant, 0, 0))
{
var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue);
if (newVariant.Id != current.CurrentItem.Variant.Id)
current.SetItem(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, current.CurrentItem.SecondaryId,
newVariant));
}
}
}
/// <summary> Draw an input for stain that can set arbitrary values instead of choosing valid stains. </summary>
private static void DrawStainArtisan(EquipDrawData data)
{
int stainId = data.CurrentStain.Id;
ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale);
if (!ImGui.InputInt("##stain", ref stainId, 0, 0))
return;
var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue);
if (newStainId != data.CurrentStain.Id)
data.SetStain(newStainId);
}
/// <summary> Draw an input for armor that can set arbitrary values instead of choosing items. </summary>
private void DrawArmorArtisan(EquipDrawData data)
{
int setId = data.CurrentItem.PrimaryId.Id;
int variant = data.CurrentItem.Variant.Id;
ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##setId", ref setId, 0, 0))
{
var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue);
if (newSetId.Id != data.CurrentItem.PrimaryId.Id)
data.SetItem(_items.Identify(data.Slot, newSetId, data.CurrentItem.Variant));
}
ImGui.SameLine();
ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##variant", ref variant, 0, 0))
{
var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue);
if (newVariant != data.CurrentItem.Variant)
data.SetItem(_items.Identify(data.Slot, data.CurrentItem.PrimaryId, newVariant));
}
}
#endregion
#region Small #region Small
@ -287,14 +205,31 @@ public class EquipmentDrawer
} }
else if (equipDrawData.IsState) else if (equipDrawData.IsState)
{ {
_advancedDyes.DrawButton(equipDrawData.Slot); _advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u);
} }
if (VerifyRestrictedGear(equipDrawData)) if (VerifyRestrictedGear(equipDrawData))
label += " (Restricted)"; label += " (Restricted)";
DrawEquipLabel(equipDrawData is { IsDesign: true, HasAdvancedDyes: true }, label);
}
private void DrawBonusItemSmall(in BonusDrawData bonusDrawData)
{
ImGui.Dummy(new Vector2(StainId.NumStains * ImUtf8.FrameHeight + (StainId.NumStains - 1) * ImUtf8.ItemSpacing.X, ImUtf8.FrameHeight));
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted(label); DrawBonusItem(bonusDrawData, out var label, true, false, false);
if (bonusDrawData.DisplayApplication)
{
ImGui.SameLine();
DrawApply(bonusDrawData);
}
else if (bonusDrawData.IsState)
{
_advancedDyes.DrawButton(bonusDrawData.Slot, bonusDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u);
}
DrawEquipLabel(bonusDrawData is { IsDesign: true, HasAdvancedDyes: true }, label);
} }
private void DrawWeaponsSmall(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) private void DrawWeaponsSmall(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons)
@ -311,12 +246,12 @@ public class EquipmentDrawer
} }
else if (mainhand.IsState) else if (mainhand.IsState)
{ {
_advancedDyes.DrawButton(EquipSlot.MainHand); _advancedDyes.DrawButton(EquipSlot.MainHand, mainhand.HasAdvancedDyes ? _advancedMaterialColor : 0u);
} }
if (allWeapons) if (allWeapons)
mainhandLabel += $" ({mainhand.CurrentItem.Type.ToName()})"; mainhandLabel += $" ({mainhand.CurrentItem.Type.ToName()})";
WeaponHelpMarker(mainhandLabel); WeaponHelpMarker(mainhand is { IsDesign: true, HasAdvancedDyes: true }, mainhandLabel);
if (offhand.CurrentItem.Type is FullEquipType.Unknown) if (offhand.CurrentItem.Type is FullEquipType.Unknown)
return; return;
@ -333,10 +268,10 @@ public class EquipmentDrawer
} }
else if (offhand.IsState) else if (offhand.IsState)
{ {
_advancedDyes.DrawButton(EquipSlot.OffHand); _advancedDyes.DrawButton(EquipSlot.OffHand, offhand.HasAdvancedDyes ? _advancedMaterialColor : 0u);
} }
WeaponHelpMarker(offhandLabel); WeaponHelpMarker(offhand is { IsDesign: true, HasAdvancedDyes: true }, offhandLabel);
} }
#endregion #endregion
@ -357,8 +292,8 @@ public class EquipmentDrawer
DrawApply(equipDrawData); DrawApply(equipDrawData);
} }
ImGui.SameLine(); DrawEquipLabel(equipDrawData is { IsDesign: true, HasAdvancedDyes: true }, label);
ImGui.TextUnformatted(label);
DrawStain(equipDrawData, false); DrawStain(equipDrawData, false);
if (equipDrawData.DisplayApplication) if (equipDrawData.DisplayApplication)
{ {
@ -367,16 +302,36 @@ public class EquipmentDrawer
} }
else if (equipDrawData.IsState) else if (equipDrawData.IsState)
{ {
_advancedDyes.DrawButton(equipDrawData.Slot); _advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u);
} }
if (VerifyRestrictedGear(equipDrawData)) if (VerifyRestrictedGear(equipDrawData))
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted("(Restricted)"); ImUtf8.Text("(Restricted)"u8);
} }
} }
private void DrawBonusItemNormal(in BonusDrawData bonusDrawData)
{
bonusDrawData.CurrentItem.DrawIcon(_textures, _iconSize, bonusDrawData.Slot);
var right = ImGui.IsItemClicked(ImGuiMouseButton.Right);
var left = ImGui.IsItemClicked(ImGuiMouseButton.Left);
ImGui.SameLine();
DrawBonusItem(bonusDrawData, out var label, false, right, left);
if (bonusDrawData.DisplayApplication)
{
ImGui.SameLine();
DrawApply(bonusDrawData);
}
else if (bonusDrawData.IsState)
{
_advancedDyes.DrawButton(bonusDrawData.Slot, bonusDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u);
}
DrawEquipLabel(bonusDrawData is { IsDesign: true, HasAdvancedDyes: true }, label);
}
private void DrawWeaponsNormal(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) private void DrawWeaponsNormal(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons)
{ {
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
@ -385,7 +340,7 @@ public class EquipmentDrawer
mainhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.MainHand); mainhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.MainHand);
var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); var left = ImGui.IsItemClicked(ImGuiMouseButton.Left);
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.Group()) using (ImUtf8.Group())
{ {
DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, false, left); DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, false, left);
if (mainhand.DisplayApplication) if (mainhand.DisplayApplication)
@ -394,7 +349,8 @@ public class EquipmentDrawer
DrawApply(mainhand); DrawApply(mainhand);
} }
WeaponHelpMarker(mainhandLabel, allWeapons ? mainhand.CurrentItem.Type.ToName() : null); WeaponHelpMarker(mainhand is { IsDesign: true, HasAdvancedDyes: true }, mainhandLabel,
allWeapons ? mainhand.CurrentItem.Type.ToName() : null);
DrawStain(mainhand, false); DrawStain(mainhand, false);
if (mainhand.DisplayApplication) if (mainhand.DisplayApplication)
@ -404,7 +360,7 @@ public class EquipmentDrawer
} }
else if (mainhand.IsState) else if (mainhand.IsState)
{ {
_advancedDyes.DrawButton(EquipSlot.MainHand); _advancedDyes.DrawButton(EquipSlot.MainHand, mainhand.HasAdvancedDyes ? _advancedMaterialColor : 0u);
} }
} }
@ -415,7 +371,7 @@ public class EquipmentDrawer
var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); var right = ImGui.IsItemClicked(ImGuiMouseButton.Right);
left = ImGui.IsItemClicked(ImGuiMouseButton.Left); left = ImGui.IsItemClicked(ImGuiMouseButton.Left);
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.Group()) using (ImUtf8.Group())
{ {
DrawOffhand(mainhand, offhand, out var offhandLabel, false, right, left); DrawOffhand(mainhand, offhand, out var offhandLabel, false, right, left);
if (offhand.DisplayApplication) if (offhand.DisplayApplication)
@ -424,7 +380,7 @@ public class EquipmentDrawer
DrawApply(offhand); DrawApply(offhand);
} }
WeaponHelpMarker(offhandLabel); WeaponHelpMarker(offhand is { IsDesign: true, HasAdvancedDyes: true }, offhandLabel);
DrawStain(offhand, false); DrawStain(offhand, false);
if (offhand.DisplayApplication) if (offhand.DisplayApplication)
@ -432,28 +388,62 @@ public class EquipmentDrawer
ImGui.SameLine(); ImGui.SameLine();
DrawApplyStain(offhand); DrawApplyStain(offhand);
} }
else if (mainhand.IsState) else if (offhand.IsState)
{ {
_advancedDyes.DrawButton(EquipSlot.OffHand); _advancedDyes.DrawButton(EquipSlot.OffHand, offhand.HasAdvancedDyes ? _advancedMaterialColor : 0u);
} }
} }
} }
private void DrawStain(in EquipDrawData data, bool small) private void DrawStain(in EquipDrawData data, bool small)
{ {
var found = _stainData.TryGetValue(data.CurrentStain, out var stain);
using var disabled = ImRaii.Disabled(data.Locked); using var disabled = ImRaii.Disabled(data.Locked);
var change = small var width = (_comboLength - ImUtf8.ItemInnerSpacing.X * (data.CurrentStains.Count - 1)) / data.CurrentStains.Count;
? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss) foreach (var (stainId, index) in data.CurrentStains.WithIndex())
: _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength); {
if (change) using var id = ImUtf8.PushId(index);
if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) var found = _stainData.TryGetValue(stainId, out var stain);
data.SetStain(stain.RowIndex); var change = small
else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) ? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss)
data.SetStain(Stain.None.RowIndex); : _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, width);
if (ResetOrClear(data.Locked, false, data.AllowRevert, true, data.CurrentStain, data.GameStain, Stain.None.RowIndex, out _)) _itemCopy.HandleCopyPaste(data, index);
data.SetStain(Stain.None.RowIndex); if (!change)
DrawStainDragDrop(data, index, stain, found);
if (index < data.CurrentStains.Count - 1)
ImUtf8.SameLineInner();
if (change)
if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain))
data.SetStains(data.CurrentStains.With(index, stain.RowIndex));
else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex)
data.SetStains(data.CurrentStains.With(index, Stain.None.RowIndex));
if (ResetOrClear(data.Locked, false, data.AllowRevert, true, stainId, data.GameStains[index], Stain.None.RowIndex,
out var newStain))
data.SetStains(data.CurrentStains.With(index, newStain));
}
}
private void DrawStainDragDrop(in EquipDrawData data, int index, Stain stain, bool found)
{
if (found)
{
using var dragSource = ImUtf8.DragDropSource();
if (dragSource.Success)
{
DragDropSource.SetPayload("stainDragDrop"u8);
_draggedStain = stain;
ImUtf8.Text($"Dragging {stain.Name}...");
}
}
using var dragTarget = ImUtf8.DragDropTarget();
if (dragTarget.IsDropping("stainDragDrop"u8) && _draggedStain.HasValue)
{
data.SetStains(data.CurrentStains.With(index, _draggedStain.Value.RowIndex));
_draggedStain = null;
}
} }
private void DrawItem(in EquipDrawData data, out string label, bool small, bool clear, bool open) private void DrawItem(in EquipDrawData data, out string label, bool small, bool clear, bool open)
@ -468,16 +458,91 @@ public class EquipmentDrawer
using var disabled = ImRaii.Disabled(data.Locked); using var disabled = ImRaii.Disabled(data.Locked);
var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
_requiredComboWidth); _requiredComboWidth);
DrawGearDragDrop(data);
if (change)
data.SetItem(combo.CurrentSelection);
else if (combo.CustomVariant.Id > 0)
data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant));
_itemCopy.HandleCopyPaste(data);
if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, ItemManager.NothingItem(data.Slot),
out var item))
data.SetItem(item);
}
private void DrawBonusItem(in BonusDrawData data, out string label, bool small, bool clear, bool open)
{
var combo = _bonusItemCombo[data.Slot.ToIndex()];
label = combo.Label;
if (!data.Locked && open)
UiHelpers.OpenCombo($"##{combo.Label}");
using var disabled = ImRaii.Disabled(data.Locked);
var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.Id.BonusItem,
small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
_requiredComboWidth);
if (ImGui.IsItemHovered() && ImGui.GetIO().KeyCtrl)
{
if (ImGui.IsKeyPressed(ImGuiKey.C))
_itemCopy.Copy(combo.CurrentSelection);
else if (ImGui.IsKeyPressed(ImGuiKey.V))
_itemCopy.Paste(data.Slot.ToEquipType(), data.SetItem);
}
if (change) if (change)
data.SetItem(combo.CurrentSelection); data.SetItem(combo.CurrentSelection);
else if (combo.CustomVariant.Id > 0) else if (combo.CustomVariant.Id > 0)
data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant));
if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, ItemManager.NothingItem(data.Slot), if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, EquipItem.BonusItemNothing(data.Slot),
out var item)) out var item))
data.SetItem(item); data.SetItem(item);
} }
private void DrawGearDragDrop(in EquipDrawData data)
{
if (data.CurrentItem.Valid)
{
using var dragSource = ImUtf8.DragDropSource();
if (dragSource.Success)
{
DragDropSource.SetPayload("equipDragDrop"u8);
_draggedItem.Update(_items, data.CurrentItem, data.Slot);
}
}
using var dragTarget = ImUtf8.DragDropTarget();
if (!dragTarget)
return;
var item = _draggedItem[data.Slot];
if (!item.Valid)
return;
_dragTarget = data.Slot;
if (!dragTarget.IsDropping("equipDragDrop"u8))
return;
data.SetItem(item);
_draggedItem.Clear();
}
public unsafe void DrawDragDropTooltip()
{
var payload = ImGui.GetDragDropPayload().Handle;
if (payload is null)
return;
if (!MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)Unsafe.AsPointer(ref payload->DataType_0)).SequenceEqual("equipDragDrop"u8))
return;
using var tt = ImUtf8.Tooltip();
if (_dragTarget is EquipSlot.Unknown)
ImUtf8.Text($"Dragging {_draggedItem.Dragged.Name}...");
else
ImUtf8.Text($"Converting to {_draggedItem[_dragTarget].Name}...");
}
private static bool ResetOrClear<T>(bool locked, bool clicked, bool allowRevert, bool allowClear, private static bool ResetOrClear<T>(bool locked, bool clicked, bool allowRevert, bool allowClear,
in T currentItem, in T revertItem, in T clearItem, out T? item) where T : IEquatable<T> in T currentItem, in T revertItem, in T clearItem, out T? item) where T : IEquatable<T>
{ {
@ -501,7 +566,7 @@ public class EquipmentDrawer
(false, true, _) => ("Right-click to clear.\nControl and mouse wheel to scroll.", clearItem, true), (false, true, _) => ("Right-click to clear.\nControl and mouse wheel to scroll.", clearItem, true),
(false, false, _) => ("Control and mouse wheel to scroll.", default, false), (false, false, _) => ("Control and mouse wheel to scroll.", default, false),
}; };
ImGuiUtil.HoverTooltip(tt); ImUtf8.HoverTooltip(tt);
return clicked && valid; return clicked && valid;
} }
@ -526,8 +591,13 @@ public class EquipmentDrawer
if (combo.Draw(mainhand.CurrentItem.Name, mainhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, if (combo.Draw(mainhand.CurrentItem.Name, mainhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
_requiredComboWidth)) _requiredComboWidth))
changedItem = combo.CurrentSelection; changedItem = combo.CurrentSelection;
else if (ResetOrClear(mainhand.Locked || unknown, open, mainhand.AllowRevert, false, mainhand.CurrentItem, mainhand.GameItem, else if (combo.CustomVariant.Id > 0 && (drawAll || ItemData.ConvertWeaponId(combo.CustomSetId) == mainhand.CurrentItem.Type))
default, out var c)) changedItem = _items.Identify(mainhand.Slot, combo.CustomSetId, combo.CustomWeaponId, combo.CustomVariant);
_itemCopy.HandleCopyPaste(mainhand);
DrawGearDragDrop(mainhand);
if (ResetOrClear(mainhand.Locked || unknown, open, mainhand.AllowRevert, false, mainhand.CurrentItem, mainhand.GameItem,
default, out var c))
changedItem = c; changedItem = c;
if (changedItem != null) if (changedItem != null)
@ -543,8 +613,9 @@ public class EquipmentDrawer
} }
} }
if (unknown && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) if (unknown)
ImGui.SetTooltip("The weapon type could not be identified, thus changing it to other weapons of that type is not possible."); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled,
"The weapon type could not be identified, thus changing it to other weapons of that type is not possible."u8);
} }
private void DrawOffhand(in EquipDrawData mainhand, in EquipDrawData offhand, out string label, bool small, bool clear, bool open) private void DrawOffhand(in EquipDrawData mainhand, in EquipDrawData offhand, out string label, bool small, bool clear, bool open)
@ -557,13 +628,17 @@ public class EquipmentDrawer
label = combo.Label; label = combo.Label;
var locked = offhand.Locked var locked = offhand.Locked
|| !_gPose.InGPose && (offhand.CurrentItem.Type is FullEquipType.Unknown || mainhand.CurrentItem.Type is FullEquipType.Unknown); || !_gPose.InGPose && (offhand.CurrentItem.Type.IsUnknown() || mainhand.CurrentItem.Type.IsUnknown());
using var disabled = ImRaii.Disabled(locked); using var disabled = ImRaii.Disabled(locked);
if (!locked && open) if (!locked && open)
UiHelpers.OpenCombo($"##{combo.Label}"); UiHelpers.OpenCombo($"##{combo.Label}");
if (combo.Draw(offhand.CurrentItem.Name, offhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, if (combo.Draw(offhand.CurrentItem.Name, offhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
_requiredComboWidth)) _requiredComboWidth))
offhand.SetItem(combo.CurrentSelection); offhand.SetItem(combo.CurrentSelection);
else if (combo.CustomVariant.Id > 0 && ItemData.ConvertWeaponId(combo.CustomSetId) == offhand.CurrentItem.Type)
offhand.SetItem(_items.Identify(mainhand.Slot, combo.CustomSetId, combo.CustomWeaponId, combo.CustomVariant));
_itemCopy.HandleCopyPaste(offhand);
DrawGearDragDrop(offhand);
var defaultOffhand = _items.GetDefaultOffhand(mainhand.CurrentItem); var defaultOffhand = _items.GetDefaultOffhand(mainhand.CurrentItem);
if (ResetOrClear(locked, clear, offhand.AllowRevert, true, offhand.CurrentItem, offhand.GameItem, defaultOffhand, out var item)) if (ResetOrClear(locked, clear, offhand.AllowRevert, true, offhand.CurrentItem, offhand.GameItem, defaultOffhand, out var item))
@ -577,9 +652,16 @@ public class EquipmentDrawer
data.SetApplyItem(enabled); data.SetApplyItem(enabled);
} }
private static void DrawApply(in BonusDrawData data)
{
if (UiHelpers.DrawCheckbox($"##apply{data.Slot}", "Apply this bonus item when applying the Design.", data.CurrentApply, out var enabled,
data.Locked))
data.SetApplyItem(enabled);
}
private static void DrawApplyStain(in EquipDrawData data) private static void DrawApplyStain(in EquipDrawData data)
{ {
if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this item when applying the Design.", data.CurrentApplyStain, if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this dye to the item when applying the Design.", data.CurrentApplyStain,
out var enabled, out var enabled,
data.Locked)) data.Locked))
data.SetApplyStain(enabled); data.SetApplyStain(enabled);
@ -587,14 +669,14 @@ public class EquipmentDrawer
#endregion #endregion
private static void WeaponHelpMarker(string label, string? type = null) private void WeaponHelpMarker(bool hasAdvancedDyes, string label, string? type = null)
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGuiComponents.HelpMarker( ImGuiComponents.HelpMarker(
"Changing weapons to weapons of different types can cause crashes, freezes, soft- and hard locks and cheating, " "Changing weapons to weapons of different types can cause crashes, freezes, soft- and hard locks and cheating, "
+ "thus it is only allowed to change weapons to other weapons of the same type."); + "thus it is only allowed to change weapons to other weapons of the same type.");
ImGui.SameLine(); DrawEquipLabel(hasAdvancedDyes, label);
ImGui.TextUnformatted(label);
if (type == null) if (type == null)
return; return;
@ -602,4 +684,17 @@ public class EquipmentDrawer
pos.Y += ImGui.GetFrameHeightWithSpacing(); pos.Y += ImGui.GetFrameHeightWithSpacing();
ImGui.GetWindowDrawList().AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), $"({type})"); ImGui.GetWindowDrawList().AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), $"({type})");
} }
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
private void DrawEquipLabel(bool hasAdvancedDyes, string label)
{
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, _advancedMaterialColor, hasAdvancedDyes))
{
ImUtf8.Text(label);
}
if (hasAdvancedDyes)
ImUtf8.HoverTooltip("This design has advanced dyes setup for this slot."u8);
}
} }

View file

@ -2,7 +2,7 @@
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Glamourer.Unlocks; using Glamourer.Unlocks;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;

View file

@ -1,12 +1,13 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.Unlocks; using Glamourer.Unlocks;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Log; using OtterGui.Log;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -75,44 +76,41 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
var ret = ImGui.Selectable(name, selected); var ret = ImGui.Selectable(name, selected);
ImGui.SameLine(); ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
ImGuiUtil.RightAlign($"({obj.ModelString})"); ImUtf8.TextRightAligned($"({obj.PrimaryId.Id}-{obj.Variant.Id})");
return ret; return ret;
} }
protected override bool IsVisible(int globalIndex, LowerString filter) protected override bool IsVisible(int globalIndex, LowerString filter)
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString()); => base.IsVisible(globalIndex, filter) || Items[globalIndex].ModelString.StartsWith(filter.Lower);
protected override string ToString(EquipItem obj) protected override string ToString(EquipItem obj)
=> obj.Name; => obj.Name;
private static string GetLabel(IDataManager gameData, EquipSlot slot) private static string GetLabel(IDataManager gameData, EquipSlot slot)
{ {
var sheet = gameData.GetExcelSheet<Addon>()!; var sheet = gameData.GetExcelSheet<Addon>();
return slot switch return slot switch
{ {
EquipSlot.Head => sheet.GetRow(740)?.Text.ToString() ?? "Head", EquipSlot.Head => sheet.TryGetRow(740, out var text) ? text.Text.ToString() : "Head",
EquipSlot.Body => sheet.GetRow(741)?.Text.ToString() ?? "Body", EquipSlot.Body => sheet.TryGetRow(741, out var text) ? text.Text.ToString() : "Body",
EquipSlot.Hands => sheet.GetRow(742)?.Text.ToString() ?? "Hands", EquipSlot.Hands => sheet.TryGetRow(742, out var text) ? text.Text.ToString() : "Hands",
EquipSlot.Legs => sheet.GetRow(744)?.Text.ToString() ?? "Legs", EquipSlot.Legs => sheet.TryGetRow(744, out var text) ? text.Text.ToString() : "Legs",
EquipSlot.Feet => sheet.GetRow(745)?.Text.ToString() ?? "Feet", EquipSlot.Feet => sheet.TryGetRow(745, out var text) ? text.Text.ToString() : "Feet",
EquipSlot.Ears => sheet.GetRow(746)?.Text.ToString() ?? "Ears", EquipSlot.Ears => sheet.TryGetRow(746, out var text) ? text.Text.ToString() : "Ears",
EquipSlot.Neck => sheet.GetRow(747)?.Text.ToString() ?? "Neck", EquipSlot.Neck => sheet.TryGetRow(747, out var text) ? text.Text.ToString() : "Neck",
EquipSlot.Wrists => sheet.GetRow(748)?.Text.ToString() ?? "Wrists", EquipSlot.Wrists => sheet.TryGetRow(748, out var text) ? text.Text.ToString() : "Wrists",
EquipSlot.RFinger => sheet.GetRow(749)?.Text.ToString() ?? "Right Ring", EquipSlot.RFinger => sheet.TryGetRow(749, out var text) ? text.Text.ToString() : "Right Ring",
EquipSlot.LFinger => sheet.GetRow(750)?.Text.ToString() ?? "Left Ring", EquipSlot.LFinger => sheet.TryGetRow(750, out var text) ? text.Text.ToString() : "Left Ring",
_ => string.Empty, _ => string.Empty,
}; };
} }
private static IReadOnlyList<EquipItem> GetItems(FavoriteManager favorites, ItemManager items, EquipSlot slot) private static List<EquipItem> GetItems(FavoriteManager favorites, ItemManager items, EquipSlot slot)
{ {
var nothing = ItemManager.NothingItem(slot); var nothing = ItemManager.NothingItem(slot);
if (!items.ItemData.ByType.TryGetValue(slot.ToEquipType(), out var list)) if (!items.ItemData.ByType.TryGetValue(slot.ToEquipType(), out var list))
return new[] return [nothing];
{
nothing,
};
var enumerable = list.AsEnumerable(); var enumerable = list.AsEnumerable();
if (slot.IsEquipment()) if (slot.IsEquipment())

Some files were not shown because too many files have changed in this diff Show more