Compare commits

...

424 commits

Author SHA1 Message Date
Ottermandias
f8ca572d38 Fix issue in unlocks tab. 2025-12-26 18:23:23 +01:00
Ottermandias
59c9601a9b Fix issue with material value in history. 2025-12-26 18:23:23 +01:00
Actions User
0d9a0d49ab [CI] Updating repo.json for 1.5.1.7 2025-12-20 15:03:30 +00:00
Ottermandias
96f825e298 Update Penumbra API. 2025-12-20 16:01:00 +01:00
Ottermandias
62c9152d8c Merge branch 'main' of github.com:Ottermandias/Glamourer 2025-12-20 16:00:10 +01:00
Actions User
98b702d6e6 [CI] Updating repo.json for 1.5.1.6 2025-12-19 13:16:42 +00:00
Ottermandias
3c68124b29 Ny 2025-12-19 14:15:14 +01:00
Ottermandias
77f3912bf2 Minor touch up ReapplyState. 2025-12-19 14:01:51 +01:00
Ottermandias
643c83a6f3 Touch up CanUnlock. 2025-12-19 13:56:36 +01:00
Ottermandias
5656d88c94 Merge branch 'refs/heads/Exter-N/penumbra-settings' 2025-12-19 01:29:19 +01:00
Ottermandias
cf87184c92 SDK update. 2025-12-19 01:29:12 +01:00
Ottermandias
598f598e82
Merge pull request #115 from CordeliaMist/ReapplyState
Add ReapplyState & ReapplyStateName
2025-12-19 01:27:19 +01:00
Ottermandias
06c593bbcb
Merge pull request #114 from Bracket416/IsUnlocked
IsUnlocked
2025-12-19 01:27:04 +01:00
Ottermandias
507e1268ac Update SDK. 2025-12-17 18:39:52 +01:00
Cordelia
da14548c43
Merge branch 'Ottermandias:main' into ReapplyState 2025-12-13 09:20:49 -08:00
Actions User
5b6517aae8 [CI] Updating repo.json for 1.5.1.5 2025-11-28 22:09:08 +00:00
Ottermandias
aadcf771e7 1.5.1.5 2025-11-28 23:06:53 +01:00
Exter-N
04fb37d661 Display relevant settings in Penumbra 2025-11-13 20:09:26 +01:00
Ottermandias
bf4673a1d9 Save Remove and Inherit for mod associations. 2025-11-07 23:30:18 +01:00
Ottermandias
76b214c643 Fix try-on interaction with Penumbra for facewear. 2025-11-07 23:30:18 +01:00
Ottermandias
434a5a809e Make old backup files overwrite instead of throwing. 2025-11-07 23:30:18 +01:00
Actions User
88fe25f69e [CI] Updating repo.json for testing_1.5.1.4 2025-10-23 15:40:25 +00:00
Ottermandias
bef1e39ac3 Update Libraries. 2025-10-23 17:37:27 +02:00
Ottermandias
c604d5dbe5 Add DeletePlayerState. 2025-10-23 17:25:59 +02:00
Ottermandias
a0d912a395 Fix issue with reverting state of unavailable actors. 2025-10-23 17:25:59 +02:00
Actions User
a56852f918 [CI] Updating repo.json for 1.5.1.3 2025-10-07 11:02:02 +00:00
Ottermandias
4228fc1b89 fu 2025-10-07 12:59:39 +02:00
Ottermandias
e644b8da28 Fix span issue. 2025-10-07 12:53:55 +02:00
Ottermandias
ace3a8f755 Again. 2025-10-07 12:43:40 +02:00
Ottermandias
76ed347cbf Update signatures. 2025-10-07 12:28:18 +02:00
Cordelia Mist
48bef12555 Optional Addition: Include IPC to get the current state of Auto-Reload Gear, and an IPC Event call for when the option changes.
- Should help clear up ambiguity with any external plugins intending to call ReapplyState on a mod-change to themselves, to know if Glamourer has it handled for them.
2025-10-05 10:31:41 -07:00
Ottermandias
c3469a1687 Fix facewear advanced dyes, fix backup service not running in task, update libraries. 2025-09-28 23:55:44 +02:00
Cordelia Mist
d6c36ca4f7 Add IPC Calls to IPC Tester. 2025-09-26 19:12:02 -07:00
Cordelia Mist
44345b9429 Init providers from API 2025-09-26 19:11:39 -07:00
Cordelia Mist
20914bc064 Add ReapplyState & ReapplyStateName with Helpers. 2025-09-26 19:11:07 -07:00
Ottermandias
0a9693daea
Update CodeService.cs 2025-09-15 20:29:13 +02:00
Bracket
8f362c5121
Update StateApi.cs 2025-09-02 02:02:52 +01:00
Bracket
0442fb7b60
Update IpcProviders.cs 2025-09-02 02:02:08 +01:00
Bracket
c62c3c4eea
Update .gitmodules 2025-09-01 11:27:35 +01:00
Bracket
6a34d41f6a
Update .gitmodules 2025-09-01 11:27:11 +01:00
Bracket
c31f6c19a6
Update StateApi.cs 2025-08-31 23:06:54 +01:00
Bracket
da1db70635
Update IpcProviders.cs 2025-08-31 23:06:23 +01:00
Bracket
c420b1f180
Update .gitmodules 2025-08-31 23:04:41 +01:00
Actions User
414bd8bee7 [CI] Updating repo.json for 1.5.1.2 2025-08-28 16:52:43 +00:00
Ottermandias
8e1745d67a Once more with feeling 2025-08-28 18:47:57 +02:00
Actions User
889f01a724 [CI] Updating repo.json for 1.5.1.1 2025-08-26 09:58:08 +00:00
Ottermandias
6e62905fa7 Fix staging incompatibility with CS. 2025-08-26 11:55:00 +02:00
Actions User
654787fa0d [CI] Updating repo.json for 1.5.1.0 2025-08-25 08:45:28 +00:00
Ottermandias
835ba23935 1.5.1.0 2025-08-25 10:43:14 +02:00
Ottermandias
389a8781d6 Update library. 2025-08-25 10:39:38 +02:00
Actions User
3eabe591df [CI] Updating repo.json for testing_1.5.0.9 2025-08-24 13:59:02 +00:00
Ottermandias
487d3b9399 Update PCP Service. 2025-08-24 15:49:29 +02:00
Actions User
4d4e4669dd [CI] Updating repo.json for testing_1.5.0.8 2025-08-22 18:34:49 +00:00
Ottermandias
fb065549e9 Add PCP Service. 2025-08-22 20:32:32 +02:00
Ottermandias
2c34154915 Update API. 2025-08-22 20:32:32 +02:00
Actions User
3704051b0f [CI] Updating repo.json for 1.5.0.7 2025-08-17 08:45:55 +00:00
Ottermandias
b2b8f2b6eb Make glamourers visor toggle trigger static visors. (?!?) 2025-08-17 10:43:26 +02:00
Ottermandias
22e6c0655b Add ear state when toggling meta application via button. 2025-08-16 11:59:08 +02:00
Ottermandias
bb2ba0cf11 Add glasses to advanced dye slot combo. 2025-08-16 11:59:08 +02:00
Ottermandias
e854386b23 Update OtterGui 2025-08-16 11:59:08 +02:00
Actions User
49d24df2e7 [CI] Updating repo.json for 1.5.0.6 2025-08-12 12:53:44 +00:00
Ottermandias
c9b291c2f3 Add new parameter to LoadWeapon hook. 2025-08-12 14:47:09 +02:00
Actions User
65f789880d [CI] Updating repo.json for 1.5.0.5 2025-08-12 10:32:03 +00:00
Ottermandias
abf998a727 Update GameData 2025-08-12 12:29:55 +02:00
Ottermandias
4cc191cb25 Add viera ear visibility to application rules. 2025-08-11 20:53:44 +02:00
Ottermandias
dc431c10a5 Add chat command to toggle automation. 2025-08-11 19:59:27 +02:00
Ottermandias
26862ba78f Update ChangedEquipData. 2025-08-11 19:59:27 +02:00
Actions User
e4374337f2 [CI] Updating repo.json for 1.5.0.4 2025-08-09 18:50:50 +00:00
Ottermandias
240c889fff Fix changed equipment access to use new size. 2025-08-09 20:48:46 +02:00
Ottermandias
612cd31c3e Fix some caravans. 2025-08-09 19:02:32 +02:00
Actions User
304b362002 [CI] Updating repo.json for 1.5.0.3 2025-08-09 16:55:27 +00:00
Ottermandias
8f34f197d0 Another try. 2025-08-09 18:53:22 +02:00
Actions User
1c97266a93 [CI] Updating repo.json for 1.5.0.2 2025-08-09 16:41:32 +00:00
Ottermandias
a9caddafd5 Maybe fix design crashes. 2025-08-09 18:39:14 +02:00
Actions User
34bf95dddb [CI] Updating repo.json for 1.5.0.1 2025-08-09 11:03:48 +00:00
Ottermandias
4761b8f584 Need staging again... 2025-08-09 13:01:48 +02:00
Ottermandias
0d94aae732 Fix popups not working early. 2025-08-09 12:11:42 +02:00
Ottermandias
e83f328cdc Fix resizable child. 2025-08-09 11:58:52 +02:00
Ottermandias
52fd29c478 Woops 2025-08-09 11:52:17 +02:00
Ottermandias
a8b79993df Make QDB ignore close hotkey. 2025-08-09 11:47:11 +02:00
Ottermandias
98574558e5 Set Repo API level to 13 and remove stg from future releases. 2025-08-08 23:07:08 +02:00
Actions User
557cbf23ce [CI] Updating repo.json for 1.5.0.0 2025-08-08 21:06:10 +00:00
Ottermandias
56753ae7ba Use staging for release. 2025-08-08 23:03:58 +02:00
Ottermandias
b66df624f7 Update Gamedata. 2025-08-08 23:01:54 +02:00
Ottermandias
be78f1447b 1.5.0.0 2025-08-08 15:56:08 +02:00
Ottermandias
97e32a3cb7 Update GameData. 2025-08-08 15:48:19 +02:00
Ottermandias
4472920536 Move Viera Ears sig to gamedata. 2025-08-08 15:46:24 +02:00
Ottermandias
ac6a726f57 Remaining API13 updates. 2025-08-08 15:39:05 +02:00
Ottermandias
00d550f4fe Add viera ear flags 2025-08-08 15:38:51 +02:00
Ottermandias
0f98fac157 Add auto-locking to design defaults. 2025-08-08 15:36:47 +02:00
Ottermandias
c25f0f72db Update for ottergui. 2025-08-08 15:36:14 +02:00
Karou
72e05e23bc stupid detached head.... 2025-08-07 21:53:01 -04:00
Karou
2c3bed6ba5 Api 13 grunt work 2025-08-07 21:50:23 -04:00
Ottermandias
d6df9885dc Update GameData. 2025-08-02 00:07:38 +02:00
Ottermandias
e7936500e0
Merge pull request #111 from CordeliaMist/Fix-RevertNotFiringFinalize
Correct StateFinalized not invoking in certain areas
2025-07-28 18:00:26 +02:00
Cordelia Mist
4ef4e65d46 Ensure that reverts called by API invoke their StateFinalizationType upon completing a reset state. 2025-07-28 08:43:13 -07:00
Cordelia Mist
40b4a8fd7a Ensure Revert via Command invokes a StateFinalization type for Reset 2025-07-28 08:37:57 -07:00
Actions User
8a1f03c272 [CI] Updating repo.json for 1.4.0.3 2025-07-14 15:12:49 +00:00
Ottermandias
8ed479eddf Fix issue with invalid bonus items. 2025-07-14 17:09:42 +02:00
Ottermandias
c0a278ca2c Make designs update on mousewheel. 2025-06-15 23:34:00 +02:00
Actions User
2e9a7004c6 [CI] Updating repo.json for 1.4.0.2 2025-06-13 15:21:04 +00:00
Ottermandias
75c76a92b9 Optimize design combos and file system. 2025-06-13 17:17:58 +02:00
Ottermandias
282935c6d6 Allow drag & drop of equipment pieces. 2025-06-03 18:40:41 +02:00
Actions User
d7b189b714 [CI] Updating repo.json for 1.4.0.1 2025-05-29 00:57:24 +00:00
Ottermandias
e3da3f356c Merge branch 'main' of github.com:Ottermandias/Glamourer 2025-05-29 02:55:23 +02:00
Ottermandias
66bed4217f Fix staining template reading. 2025-05-29 02:55:20 +02:00
Ottermandias
56bbf6593a Fix button counting. 2025-05-29 02:55:11 +02:00
Actions User
b8e1e7c384 [CI] Updating repo.json for 1.4.0.0 2025-05-28 11:58:09 +00:00
Ottermandias
a0d2c39f45 1.4.0.0 2025-05-28 13:56:06 +02:00
Ottermandias
07df3186c2 Better. 2025-05-27 14:38:04 +02:00
Ottermandias
5b59e74417 Use CS ReadStainingTemplate again. 2025-05-27 12:06:29 +02:00
Ottermandias
b4485f028d Batch some multi-design changes to skip unnecessary computations. 2025-05-23 15:21:14 +02:00
Ottermandias
74674cfa0c Make combos start from preview selection if possible. 2025-05-23 10:47:47 +02:00
Ottermandias
f192c17c9b Allow drag & drop for color buttons. 2025-05-21 17:46:56 +02:00
Ottermandias
aa1ac29182 Make design selector resizable. 2025-05-21 17:16:55 +02:00
Ottermandias
081ac6bf8b Split ResetAdvanced into two parts. 2025-05-21 17:09:41 +02:00
Ottermandias
e4b32343ae Update libraries. 2025-05-21 17:08:17 +02:00
Ottermandias
c93370ec92 Again. 2025-05-09 00:06:19 +02:00
Ottermandias
9abd7f2767 Make Dynamis IPC working. 2025-05-08 23:44:16 +02:00
Ottermandias
8a9877bb01 Add testing Dynamis IPC for debugging. 2025-05-06 00:31:26 +02:00
Ottermandias
c1e1476fa6 Fix some issues with glamourer not searching mods by name. 2025-05-06 00:30:47 +02:00
Ottermandias
b1abbb8e77 Support model id input in weapon combo, support middle-mouse pipette. 2025-05-06 00:30:27 +02:00
Ottermandias
fcb0660def Implement new IPC methods and API 1.6 2025-05-04 00:36:39 +02:00
Ottermandias
a6073e2a42 Update old PR slightly. 2025-05-03 23:36:03 +02:00
Ottermandias
2c87077918
Merge pull request #107 from Diorik/random_no_repeat
Prevent Random Repeats
2025-05-03 23:29:29 +02:00
Ottermandias
4e0a9f62b9
Merge pull request #109 from Caraxi/api-fix
Fix `SetMetaState` and `SetMetaStateName` not being registered
2025-05-02 17:28:10 +02:00
Actions User
39636f5293 [CI] Updating repo.json for 1.3.8.6 2025-05-01 21:04:23 +00:00
Ottermandias
b53124e708 Temporarily use custom address for ReadStainingTemplate. 2025-05-01 23:02:08 +02:00
Ottermandias
9a684c9ff5 Implement GetDesignListExtended. 2025-04-29 23:50:29 +02:00
Ottermandias
c7d1620c1e Update GameData. 2025-04-19 23:09:48 +02:00
Ottermandias
325b54031c Update libraries. 2025-04-19 22:28:06 +02:00
Caraxi
155a9d6266 Fix SetMetaState and SetMetaStateName not being registered 2025-04-15 14:26:30 +09:30
Actions User
4f6fb44f79 [CI] Updating repo.json for 1.3.8.5 2025-04-10 14:42:24 +00:00
Ottermandias
bfce99859f Update GameData. 2025-04-10 16:39:04 +02:00
Ottermandias
6c556d6a61 Update API. 2025-04-10 16:20:15 +02:00
Ottermandias
7ed42005dd Force higher Penumbra API version and use better IPC for cutscene parents and game objects. 2025-04-09 15:11:04 +02:00
Ottermandias
aad978f5f6 Pass actor in GearsetDataLoaded instead of looking it up. 2025-04-08 22:53:59 +02:00
Ottermandias
c0ad4aab51 Update for new ActorObjectManager. 2025-04-05 18:48:39 +02:00
Ottermandias
8fe0ac8195 Fix weapon color set issue. 2025-04-05 15:12:27 +02:00
Ottermandias
46f8818cee Add Incognito Modifier. 2025-04-04 22:35:48 +02:00
Actions User
118f51cc64 [CI] Updating repo.json for 1.3.8.4 2025-04-02 23:07:10 +00:00
Ottermandias
096d82741d Fix previous fix for weapons. 2025-04-03 01:04:58 +02:00
Actions User
b98cb31fd2 [CI] Updating repo.json for 1.3.8.3 2025-04-02 21:47:24 +00:00
Ottermandias
90813ce030 Update GameData. 2025-04-02 23:45:28 +02:00
Ottermandias
95bc52b2bc Check for valid humanity. 2025-04-02 23:45:07 +02:00
Ottermandias
d79e4b5853
Merge pull request #108 from keifufu/fix-linux-build
Fix linux build
2025-03-31 13:03:43 +02:00
keifufu
a40a6905be
newline be gone 2025-03-31 13:02:19 +02:00
keifufu
6b3a64ce14
fix linux build 2025-03-31 13:00:51 +02:00
Actions User
4fca1600ca [CI] Updating repo.json for 1.3.8.2 2025-03-29 17:06:25 +00:00
Ottermandias
b1e65e6f9d Fix some offsets. 2025-03-29 18:04:18 +01:00
Actions User
381b23fe0c [CI] Updating repo.json for 1.3.8.1 2025-03-28 16:29:25 +00:00
Ottermandias
782c4446b2 Update GameData. 2025-03-28 17:25:34 +01:00
Ottermandias
76eaa75d04 Update Penumbra.Api. 2025-03-28 17:23:25 +01:00
Ottermandias
361ed536a3 Fix issue with NPC automation due to missing job detection. 2025-03-28 17:22:57 +01:00
Ottermandias
b1d00e9812 Update GameData. 2025-03-28 15:54:26 +01:00
Ottermandias
b0abf865cb Change build step. 2025-03-28 15:54:10 +01:00
Ottermandias
d398381b52 Revert Dalamud staging on release, and update api level. 2025-03-28 14:17:16 +01:00
Actions User
d75d70bee5 [CI] Updating repo.json for 1.3.8.0 2025-03-28 13:16:18 +00:00
Ottermandias
296f1e90b5 🤷 2025-03-28 14:13:34 +01:00
Ottermandias
9d3dfbbece use staging build for release for now. 2025-03-28 14:05:53 +01:00
Ottermandias
d6d592f099 1.3.8.0 2025-03-28 13:58:19 +01:00
Ottermandias
00cb1b6643 Use CS sig. 2025-03-28 13:52:15 +01:00
Ottermandias
22babad789 Update. 2025-03-27 18:11:08 +01:00
Ottermandias
18ff905746 Add a chat command to clear temporary settings made by Glamourer. 2025-03-11 18:06:22 +01:00
Ottermandias
fd0d761b92 Fix small issue with invisible customizations applying. 2025-03-11 00:58:05 +01:00
Actions User
750d4f9eca [CI] Updating repo.json for 1.3.7.1 2025-03-10 22:59:41 +00:00
Ottermandias
25517525c9 Keep temporary mod settings manually applied by Glamourer when redrawing with automation changes. 2025-03-10 23:55:29 +01:00
Actions User
99a8e1e8c5 [CI] Updating repo.json for 1.3.7.0 2025-03-09 22:17:20 +00:00
Ottermandias
381b2a1b8b 1.3.7.0 2025-03-09 23:14:19 +01:00
Ottermandias
3dee511b9a Update some obsoletes. 2025-03-09 23:13:33 +01:00
Ottermandias
e93c3b7bb8 Update submodules. 2025-03-09 23:12:21 +01:00
Ottermandias
773682838e Make customize buttons not change advanced customization to On by default. 2025-03-09 13:37:27 +01:00
Ottermandias
c9f00c6369 Mark designs containing advanced dyes, mod associations or links in automation. 2025-03-04 16:28:48 +01:00
Ottermandias
2026069ed3 Make Glamourer able to export and import color sets to and from Penumbra, maybe. 2025-03-04 15:34:39 +01:00
Actions User
ae093506c1 [CI] Updating repo.json for testing_1.3.6.6 2025-03-03 23:21:09 +00:00
Ottermandias
87f1b613f9 Add application shortcuts to multi design panel. 2025-03-04 00:18:41 +01:00
Ottermandias
71e15474b2 Add deletion of advanced dyes to multi design selection. 2025-03-03 18:32:52 +01:00
Ottermandias
3fd6108fa1 Add buttons to remove, enable and disable multiple advanced dyes at once in a design. 2025-03-03 18:12:42 +01:00
Ottermandias
b9e4c144c2 Add some buttons to set design application rules to some presets. 2025-03-03 17:55:38 +01:00
Ottermandias
6e685b96d1 Make all panels configurable. 2025-03-03 16:31:15 +01:00
Ottermandias
c96f009fb4 Slightly speed up QDB drawing. 2025-03-03 16:31:15 +01:00
Actions User
3112079776 [CI] Updating repo.json for testing_1.3.6.5 2025-03-02 12:40:58 +00:00
Ottermandias
7015737d88 Meh. 2025-03-02 13:38:58 +01:00
Ottermandias
94f6e870e6 Update Submodules. 2025-03-02 13:33:41 +01:00
Ottermandias
528aae7eee Remove PalettePlusChecker and config options to disable Advanced Customization and Advanced Dyes. 2025-03-02 13:24:51 +01:00
Ottermandias
425c9471fb Highlight materials with advanced dyes even if inactive, allow removing inactive. 2025-02-23 23:19:24 +01:00
Ottermandias
0c8110e15e Highlight existing advanced dyes in state and design. 2025-02-21 00:10:36 +01:00
Actions User
8a7ec45bbf [CI] Updating repo.json for testing_1.3.6.3 2025-02-19 23:19:21 +00:00
Ottermandias
c1f84b4303 Differentiate between temporary settings through manual and automatic application. 2025-02-20 00:16:55 +01:00
Ottermandias
5a9e9513f4 Skip automatic updates from temporary settings when applied by design. 2025-02-20 00:16:55 +01:00
Ottermandias
9e7679e70f Move check for valid model to actor display instead of identifier list. 2025-02-20 00:16:55 +01:00
Actions User
e5b2114ac2 [CI] Updating repo.json for testing_1.3.6.2 2025-02-18 14:13:13 +00:00
Ottermandias
1168460942 Add button to reset glamourer temporary settings to qdb. 2025-02-17 17:40:56 +01:00
Actions User
f7d6e75a9b [CI] Updating repo.json for testing_1.3.6.1 2025-02-13 15:43:08 +00:00
Ottermandias
d56c2db547 Add more mod association and modded utility. 2025-02-13 16:32:23 +01:00
Ottermandias
ab2a3f5bd9 Add new colors. 2025-02-13 16:30:35 +01:00
Ottermandias
4748d1e211 Allow filtered item names. 2025-02-13 16:30:24 +01:00
Diorik
5ca151b675 PreventRandom use WeakReference, reroll rand instead of changing list 2025-02-06 13:29:47 -06:00
Diorik
67fd65d366 Make PreventRandomc figurable, clean up logic
Will no longer hold design reference or make redundant copy of list
2025-02-06 12:49:23 -06:00
Diorik
45981f2fee
Merge branch 'Ottermandias:main' into random_no_repeat 2025-02-06 11:26:07 -06:00
Ottermandias
1df2a46884 Update Submodule Versions. 2025-02-06 17:00:31 +01:00
Actions User
2c7b7d59be [CI] Updating repo.json for 1.3.6.0 2025-02-06 15:55:34 +00:00
Ottermandias
d849506ecd 1.3.6.0 2025-02-06 16:52:59 +01:00
Ottermandias
47f2cfc210
Merge pull request #105 from Diorik/redraw_command
Add "Reset Design" command
2025-02-06 16:38:10 +01:00
Ottermandias
5d1c65cce7 Fix overwriting with current state keeping old advanced dyes. 2025-02-06 16:27:10 +01:00
Diorik
cf308fc118 Prevent repeating random design
Cache the last selected random design and prevent it from being chosen again.
2025-02-04 01:54:34 -06:00
Diorik
87016419c5 Add "Reset Design" command
A new command to reapply automation while resetting the random design regardless of settings.
2025-02-04 00:46:12 -06:00
Ottermandias
da46705b52 Use new settings API. 2025-01-31 16:06:50 +01:00
Ottermandias
9a1cf3d9e6 Better 2025-01-30 22:45:59 +01:00
Ottermandias
8e0908dbf7 Add more settings to multi design panel. 2025-01-30 22:39:57 +01:00
Ottermandias
1f255a98e6 Fix issue with scaling after zone change. 2025-01-30 13:55:28 +01:00
Ottermandias
2a067ef60b Accept all existing facepaints. 2025-01-30 13:36:13 +01:00
Actions User
b3afa2067c [CI] Updating repo.json for testing_1.3.5.5 2025-01-25 13:02:30 +00:00
Ottermandias
630c4dd894 Update Submodules. 2025-01-25 14:00:36 +01:00
Ottermandias
f1fa5c2fa9 Merge branch 'refs/heads/CordeliaMist/PR-SetMetaState' 2025-01-24 23:29:03 +01:00
Ottermandias
f6c9ecad60 Move MetaFlag completely to API, rename, cleanup. 2025-01-24 23:28:23 +01:00
Cordelia Mist
5eb545f62d Namechange: SetMetaState -> SetMeta
Help clear up confusion on if it is in the StateApi section or the ItemsApi section (Which it is currently in)
2025-01-24 13:09:49 -08:00
Cordelia Mist
0f127a557d Introduced locks, if nessisary for change. 2025-01-24 11:44:09 -08:00
Cordelia Mist
611200d311 Added setMetaState by name 2025-01-24 11:43:26 -08:00
Cordelia Mist
0ed4603ba5 Corrected Paramater fields to include ApplyFlags and removed unessisary using. 2025-01-24 11:42:52 -08:00
Cordelia Mist
439849edfa Append SetMetaState with object index. 2025-01-24 11:35:53 -08:00
Cordelia Mist
091aadd4a6 Add Yield return Indices List for setter helper 2025-01-24 11:25:40 -08:00
Cordelia Mist
356b814102 Append conversion for SetMetaFlag -> MetaIndex for help in moving down the API call to the state editor call. 2025-01-24 11:14:50 -08:00
Actions User
cd6a6a462d [CI] Updating repo.json for testing_1.3.5.4 2025-01-24 16:54:22 +00:00
Ottermandias
0123fe1fbd Increment minor API version. 2025-01-24 17:52:06 +01:00
Ottermandias
8b518d3c6f Merge branch 'refs/heads/CordeliaMist/main' 2025-01-24 17:51:11 +01:00
Ottermandias
d9f9937d41 Continue renames, some cleanup. 2025-01-24 17:50:58 +01:00
Ottermandias
7be283ca30 Apply API renames. 2025-01-24 16:50:29 +01:00
Ottermandias
748c324acf Merge branch 'main' into CordeliaMist/main 2025-01-24 15:13:46 +01:00
Ottermandias
2916669319 Change EquipGearsetInternal to CS. 2025-01-24 15:06:29 +01:00
Ottermandias
30468e0b09 Fix checkbox not working. 2025-01-24 15:04:47 +01:00
Ottermandias
70918e5393 Add AutoDesignSet toggle for resetting settings. 2025-01-24 15:04:47 +01:00
Cordelia Mist
c43ce9d978 Updated new functionality and internal gearset to use FFXIVClientStructs member functions and implemented structs, corrected remaining spacing issues. 2025-01-21 10:47:12 -08:00
Cordelia
2e11481276
Merge branch 'Ottermandias:main' into main 2025-01-20 11:33:55 -08:00
Actions User
f1b335e794 [CI] Updating repo.json for testing_1.3.5.3 2025-01-20 16:49:52 +00:00
Ottermandias
96dca5dbe7 Cherry-Pick from ebdfd3f8 to use EquipGearSetInternal. 2025-01-20 17:47:36 +01:00
Ottermandias
4db0822443 Update GameData. 2025-01-20 15:40:14 +01:00
Ottermandias
cdc67a035c Check for ENPC player copies and treat them as transformations. 2025-01-20 15:40:14 +01:00
Ottermandias
8add6e5519 Fix some .net update issues. 2025-01-20 15:40:14 +01:00
Cordelia Mist
ebdfd3f8bc Correct spacing and formatting further to align with main Glamourer branch preferences. 2025-01-19 09:50:34 -08:00
Cordelia Mist
1cd8e5fb7e Corrected comments on StateApi for PR 2025-01-19 09:35:10 -08:00
Cordelia Mist
1d185e9bfe Added Proper Unsubsribe from OnStateUpdated.
Ensures that ReapplyAutomation is called from OnAutomationChange when change occurs.
Reformatted temporary Signature locations and comments to align with the structure of the respective classes.
2025-01-19 09:07:43 -08:00
Cordelia Mist
9c57935a87 removed unessisary usings and corrected gearsetDataLoaded stateListener ref. 2025-01-17 18:52:16 -08:00
Cordelia Mist
8b609e5f05 corrected comments and formatting to reflect Glamourer's main branch. 2025-01-17 18:43:05 -08:00
Cordelia Mist
e1a41b5f3c Implements true endpoints for all glamourer operations, also correctly marks reverts and gearsets. Replaced back excessive logging to maintain with logging formats expected by glamourer. 2025-01-17 18:43:05 -08:00
Cordelia Mist
c605d19510 Progress made. May be successful! 2025-01-17 18:39:55 -08:00
Actions User
8160f420db [CI] Updating repo.json for testing_1.3.5.2 2025-01-15 17:05:57 +00:00
Ottermandias
60a1ee728a Use current temporary settings for mod associations if they exist. 2025-01-14 14:51:36 +01:00
Ottermandias
c83ddf054a Add counts to multi design selection. 2025-01-12 00:04:00 +01:00
Actions User
02bfd17794 [CI] Updating repo.json for 1.3.5.1 2025-01-11 12:49:40 +00:00
Ottermandias
7d490f9cfb Fix designs not resetting temporary settings if they do not have mod associations. 2025-01-11 13:47:39 +01:00
Actions User
c541fd62c4 [CI] Updating repo.json for 1.3.5.0 2025-01-10 19:16:32 +00:00
Ottermandias
2e038350ef 1.3.5.0 2025-01-10 20:13:51 +01:00
Ottermandias
157a5b150b Update Submodules. 2025-01-10 20:01:46 +01:00
Ottermandias
8a87ff16b4 Improve mod associations display. 2025-01-10 20:01:46 +01:00
Ottermandias
3d6d04dde1 Fix bug in automation tab setting gearsets. 2025-01-10 20:01:46 +01:00
Ottermandias
d675cdc804 Improve scaling of advanced dye window. 2025-01-10 20:01:46 +01:00
Actions User
6475c3c65b [CI] Updating repo.json for testing_1.3.4.4 2024-12-31 17:06:22 +00:00
Ottermandias
d57743763b Update OtterGui. 2024-12-31 18:03:56 +01:00
Ottermandias
9b9e356ad1 Update Submodules. 2024-12-31 16:36:05 +01:00
Ottermandias
24452f3c79 Add initial support for setting temporary mod settings. 2024-12-31 16:35:08 +01:00
Ottermandias
e41755ed7e Freeze the application buttons at the top of the panels. 2024-12-31 15:16:14 +01:00
Ottermandias
71e80740f6 Add selection designs to chat commands. 2024-12-31 13:46:10 +01:00
Ottermandias
70cf21cf57 Accept more colors in designs as valid (independently of clan/gender and mixing highlights/hair and both eyes). 2024-12-29 15:48:54 +01:00
Ottermandias
ebd3fb3fbf Update submodules. 2024-12-29 13:38:21 +01:00
Ottermandias
56d014e14f Fix ImGui Assertion. 2024-12-25 22:21:58 +01:00
Ottermandias
664b42eee8 Update GameData. 2024-12-17 18:05:07 +01:00
Ottermandias
a116243900 Maybe fix disabling auto designs. 2024-12-16 17:23:04 +01:00
Ottermandias
33bf5c5392 Update GameData versioning. 2024-12-16 17:23:04 +01:00
Ottermandias
f3eb542940 Add option to always reset random designs. 2024-12-16 17:23:04 +01:00
Ottermandias
b90e68fbaf Fix inverted application of apply flags in IPC 2024-12-16 17:23:04 +01:00
Actions User
c951854d5a [CI] Updating repo.json for 1.3.4.3 2024-12-13 17:14:14 +00:00
Ottermandias
f424857bd3 Again. 2024-12-13 15:42:49 +01:00
Ottermandias
27e9223c81 Again 2024-12-09 21:29:35 +01:00
Ottermandias
31cd812519 Update GameData. 2024-12-09 21:23:33 +01:00
Ottermandias
c9febe2c74 Try to make random designs in automation stick around when redrawing/changing zone. 2024-11-30 22:11:16 +01:00
Actions User
467dc2c22f [CI] Updating repo.json for 1.3.4.2 2024-11-29 16:36:00 +00:00
Ottermandias
4215e0ad44 Add default settings for design configuration. 2024-11-27 23:44:41 +01:00
Ottermandias
533c53fd8d Fix some issues with Crests 2024-11-27 23:44:41 +01:00
Ottermandias
66ed721105 Treat no Automation Set as empty automation set. 2024-11-27 23:44:41 +01:00
Actions User
9e09d64c66 [CI] Updating repo.json for 1.3.4.1 2024-11-25 16:00:32 +00:00
Ottermandias
7f95726cc3 Do not use DX calls to read color tables except in UI. 2024-11-25 16:53:33 +01:00
Ottermandias
22c7e32760 Fix some context menu things. 2024-11-24 12:18:04 +01:00
Ottermandias
40a684ff46 Fix CalculateHeight. 2024-11-23 13:58:24 +01:00
Ottermandias
9ed8c9517b
Update repo.json 2024-11-22 14:44:29 +01:00
Actions User
3722df199b [CI] Updating repo.json for 1.3.4.0 2024-11-22 13:29:49 +00:00
Ottermandias
5464fa3a0f 1.3.4.0 2024-11-22 14:27:39 +01:00
Ottermandias
525f65e70a Fix issues with shared weapon types. 2024-11-22 14:27:39 +01:00
Actions User
b44a147fdd [CI] Updating repo.json for testing_1.3.3.3 2024-11-20 13:01:59 +00:00
Ottermandias
86d370226a Update weapon changed data offset. 2024-11-20 11:50:44 +01:00
Actions User
f9c7e567b6 [CI] Updating repo.json for testing_1.3.3.2 2024-11-17 23:31:26 +00:00
Ottermandias
7605c7cb29 Fix screen actor indices. 2024-11-18 00:29:31 +01:00
Actions User
0ba9f538ba [CI] Updating repo.json for testing_1.3.3.1 2024-11-17 21:04:05 +00:00
Ottermandias
de9d605fb0 1.3.3.1 2024-11-17 22:01:36 +01:00
Ottermandias
db34255127 Fix customization bug. 2024-11-17 22:00:50 +01:00
Ottermandias
60c7debb5e Fix offset 2024-11-17 18:13:20 +01:00
Ottermandias
c7b9791a6a Not yet increment API level. 2024-11-17 14:29:11 +01:00
Ottermandias
fe028e5ec7 Fixes. 2024-11-17 14:27:14 +01:00
Ottermandias
2ce8076e9a Current State. 2024-11-17 00:51:57 +01:00
Ottermandias
a5998b84ba Fix issues with ResetAdvancedDyes and weapon types. 2024-11-08 10:20:36 +01:00
Actions User
dd217a2475 [CI] Updating repo.json for 1.3.3.0 2024-10-18 14:26:43 +00:00
Ottermandias
4738830b8a 1.3.3.0 2024-10-18 16:02:30 +02:00
Ottermandias
c49102959f Add tails and Height 255 to available NPC options. 2024-10-18 15:34:50 +02:00
Ottermandias
1d974a0c6c
Merge pull request #101 from anya-hichu/main
Fix inverted log and table width to not hide the final "s" from "Reset Advanced Dyes"
2024-10-14 20:40:44 +02:00
Anya
dee79c121b
Merge branch 'Ottermandias:main' into main 2024-10-14 20:39:10 +02:00
Actions User
667ff2490d [CI] Updating repo.json for testing_1.3.2.2 2024-10-13 12:42:47 +00:00
Ottermandias
87d8876972 Fix some bonus item related things. 2024-10-13 14:40:43 +02:00
Actions User
530166b81a [CI] Updating repo.json for testing_1.3.2.1 2024-10-12 13:16:00 +00:00
Ottermandias
6db4a9623b Update GameData. 2024-10-12 15:14:03 +02:00
anya-hichu
11111817d7 Fix DesignInfoTable width 2024-10-12 15:02:55 +02:00
anya-hichu
da944b50cc Fix inverted log 2024-10-12 14:48:57 +02:00
Ottermandias
cca2bf645f Fix PR. 2024-10-12 13:02:30 +02:00
Ottermandias
ef96dadba5
Merge pull request #100 from anya-hichu/main
Add ResetMaterials option to Design
2024-10-12 12:03:20 +02:00
anya-hichu
210bca4c7c Add ResetMaterials option to Design 2024-10-11 19:55:05 +02:00
Ottermandias
9d99d936aa Remove BonusItem and replace with normal EquipItems everywhere. 2024-10-11 18:18:33 +02:00
Ottermandias
415ac63767 Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-10-07 18:35:22 +02:00
Ottermandias
8f53253bae Add special filters to actor selector. 2024-10-07 18:35:09 +02:00
Ottermandias
d104b794ae Add owned NPC button for automation identifiers. 2024-10-07 18:34:52 +02:00
Actions User
ea8b23d3fd [CI] Updating repo.json for 1.3.2.0 2024-10-06 13:05:46 +00:00
Ottermandias
65e33d91ac 1.3.2.0 2024-10-06 15:03:58 +02:00
Ottermandias
ce02c64655 Update Gamedata. 2024-10-06 13:01:28 +02:00
Ottermandias
83e1476e7f Make unlocks tab non-docking. 2024-10-06 12:59:15 +02:00
Ottermandias
885063d389 Make item combos search for entire model string. 2024-10-06 12:59:06 +02:00
Ottermandias
816e88e0bd Make the advanced dye window non-docking. 2024-10-06 12:51:35 +02:00
Ottermandias
726eb52e4f Update Gamedata. 2024-10-06 12:46:50 +02:00
Ottermandias
5da0738147 Add a debug rider for actors. 2024-10-06 12:45:58 +02:00
Ottermandias
9b5271bca4 Update Gamedata. 2024-10-06 11:58:39 +02:00
Ottermandias
c8cc375d4b
Update WorldSets.cs 2024-10-05 02:15:23 +02:00
Ottermandias
be9c3eab40 Add bonus item import from .chara files, fix small bugs when applying designs. 2024-09-21 21:17:24 +02:00
Ottermandias
1f1b04bdfe Update actions. 2024-09-16 23:34:29 +02:00
Ottermandias
f473abb99c Fix chat log context menu try-on. 2024-09-16 22:53:35 +02:00
Ottermandias
9a02ba2987 Add support for previewing non-existing items from Penumbra. 2024-08-31 20:51:38 +02:00
Ottermandias
03043ba2c9 Fix reversion. 2024-08-24 20:40:26 +02:00
Ottermandias
82536c75c9 Fix fun module RNG. 2024-08-24 20:40:02 +02:00
Ottermandias
773f526fba Improve handling for bonus items in designs. 2024-08-15 17:23:51 +02:00
Ottermandias
fc6604fd5a Remove Artisan code. 2024-08-15 15:58:04 +02:00
Ottermandias
a7c5d513d4 Maybe fix weapon hidden state when leaving gpose or changing zones. 2024-08-13 15:27:55 +02:00
Actions User
38a489d7f0 [CI] Updating repo.json for 1.3.1.1 2024-08-10 10:27:38 +00:00
Ottermandias
76cdacf80f Update non-testing Dalamud API Level 2024-08-10 12:24:29 +02:00
Actions User
3880c1870b [CI] Updating repo.json for 1.3.1.0 2024-08-10 10:02:21 +00:00
Ottermandias
6a46a410f7 Update Penumbra API, increment API version. 2024-08-10 11:57:44 +02:00
Ottermandias
5971592217 Add BonusItem API. 2024-08-10 11:52:57 +02:00
Ottermandias
9e06125092 Update GameData. 2024-08-10 00:57:10 +02:00
Ottermandias
3a07acbd05 1.3.1.0 2024-08-09 23:50:59 +02:00
Ottermandias
edc54203b5 Fix display and initial values of design color rows. 2024-08-09 22:09:01 +02:00
Ottermandias
af58a52a59 Update display of saved material values. 2024-08-09 16:27:18 +02:00
Actions User
2282f3f87a [CI] Updating repo.json for testing_1.3.0.11 2024-08-09 14:04:22 +00:00
Ottermandias
5cd224b164 Add design migration for inverted gloss and specular, and a backup before doing that. 2024-08-09 16:01:51 +02:00
Ottermandias
d7074c5791 Improve Gloss/Specular display 2024-08-09 16:01:51 +02:00
Actions User
d594082165 [CI] Updating repo.json for testing_1.3.0.10 2024-08-07 15:59:50 +00:00
Ottermandias
1c8d01bdd3 For later. 2024-08-07 17:57:49 +02:00
Ottermandias
a1b455d9a5 Update advanced dyes. 2024-08-07 17:55:23 +02:00
Ottermandias
61cb46a298 Update for GameData update. 2024-08-06 18:25:00 +02:00
Ottermandias
2e52030c31 Fix issue with adding mods from clipboard 2024-08-06 17:39:34 +02:00
Ottermandias
5bca4b9118 Do not apply Nothing-shield for swords periphery. 2024-08-06 17:38:53 +02:00
Ottermandias
1cc7c2f0cd Add editable mod state and priority. 2024-08-05 14:48:43 +02:00
Ottermandias
6115d24775 Some changes for codes. 2024-08-04 14:36:00 +02:00
Ottermandias
68bffba3cd Update for submodules. 2024-08-04 14:14:10 +02:00
Ottermandias
e2ffcea0b2 Fix some warnings. 2024-08-04 12:40:00 +02:00
Ottermandias
9f04ee7695 Fix visor state toggle. 2024-08-04 12:39:01 +02:00
Ottermandias
f69915dcb3 Remove some warnings. 2024-08-04 12:38:38 +02:00
Ottermandias
34caf29b32 Add more Corgis to Glamourer. 2024-08-04 12:38:04 +02:00
Ottermandias
3a63a1b22d Add some codes. 2024-08-03 02:25:53 +02:00
Ottermandias
5460ffd0a0 Update world sets. 2024-08-02 17:02:40 +02:00
Actions User
e516fd9318 [CI] Updating repo.json for testing_1.3.0.9 2024-08-01 15:08:33 +00:00
Ottermandias
ad79d9cc07 Set focus on detached advanced dyes when opening or toggling with buttons. 2024-08-01 17:06:10 +02:00
Ottermandias
ab771fd010 Add Undo to design panel. 2024-07-31 20:57:50 +02:00
Ottermandias
d8085dc022 Introduce history. 2024-07-31 19:33:23 +02:00
Ottermandias
f6434cbc61 Rename stains. 2024-07-31 17:06:30 +02:00
Ottermandias
9d569266b5 Improve job filter in unlocks tab. 2024-07-31 16:58:33 +02:00
Ottermandias
6446309bd7 Allow drag&drop on stains. 2024-07-30 20:24:05 +02:00
Actions User
d0d518ddc2 [CI] Updating repo.json for testing_1.3.0.8 2024-07-28 12:14:04 +00:00
Ottermandias
e87b216541 Rename material stuff. 2024-07-28 14:11:56 +02:00
Ottermandias
1e0b7fdfce Improve respecting of design write protection. 2024-07-28 01:33:37 +02:00
Ottermandias
1bee5c680b Add Facewear to application rules. 2024-07-27 13:59:39 +02:00
Actions User
029cf12bed [CI] Updating repo.json for testing_1.3.0.7 2024-07-26 13:37:25 +00:00
Ottermandias
ff96e51963 Maybe fix another issue with monk offhands. 2024-07-26 15:35:25 +02:00
Ottermandias
60302c37cd Fix minion placement. 2024-07-26 15:06:51 +02:00
Ottermandias
fb2b676ff0 Remove no longer needed helpers. 2024-07-26 15:06:29 +02:00
Actions User
d256702005 [CI] Updating repo.json for testing_1.3.0.6 2024-07-22 14:06:34 +00:00
Ottermandias
b2bb50b3d9 Maybe fix an issue with monk fist weapons again. 2024-07-22 16:04:23 +02:00
Actions User
b1c1cf0f99 [CI] Updating repo.json for testing_1.3.0.5 2024-07-20 21:44:33 +00:00
Ottermandias
a7f36da3f5 Fix statesource overwriting data for characterweapon. 2024-07-20 23:42:29 +02:00
Ottermandias
94b7ea2d9d Fix changed data offset for weapon. 2024-07-20 23:42:04 +02:00
Ottermandias
55f2053fe6 Fix BodyType not applying. 2024-07-20 22:01:21 +02:00
Ottermandias
a885411a8c Fix saving of favorites. 2024-07-20 21:47:00 +02:00
Ottermandias
3ad67f661a Add Hrothgar face hacks to race changing fixing of values. 2024-07-20 21:41:49 +02:00
Actions User
ed329ec989 [CI] Updating repo.json for testing_1.3.0.4 2024-07-19 15:30:58 +00:00
Ottermandias
f669616616 Update GameData. 2024-07-19 17:28:57 +02:00
Ottermandias
2f95a4ea34 Update Advanced Dyes. 2024-07-19 17:27:30 +02:00
Actions User
de9fb1fd9f [CI] Updating repo.json for testing_1.3.0.3 2024-07-17 16:41:59 +00:00
Ottermandias
8b10b3fdfb Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-07-17 18:35:48 +02:00
Ottermandias
dae3fbc901 Update single-setter for 0x10 Vector3, too. 2024-07-17 18:35:34 +02:00
Actions User
608ab7beb9 [CI] Updating repo.json for testing_1.3.0.2 2024-07-17 13:05:02 +00:00
Ottermandias
0320da0fc5 Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-07-17 02:36:31 +02:00
Ottermandias
124656c22e Fix advanced customize. 2024-07-17 02:36:01 +02:00
Actions User
1725dbb583 [CI] Updating repo.json for testing_1.3.0.1 2024-07-17 00:03:10 +00:00
Ottermandias
b3818a90df Fix design migrations and cma import. 2024-07-17 02:01:14 +02:00
Ottermandias
e7d5cf02e6 Merge branch 'main' of github.com:Ottermandias/Glamourer 2024-07-17 00:31:40 +02:00
Ottermandias
3484a29f7a Disable shine parameter application rules. 2024-07-17 00:30:16 +02:00
Ottermandias
a807c90885 Disable the currently outdated preparecolorset hook. 2024-07-17 00:29:53 +02:00
Actions User
951c167058 [CI] Updating repo.json for testing_1.3.0.0 2024-07-16 21:45:00 +00:00
Ottermandias
25491adbe3 Rename Sclera to Limbal 2024-07-16 23:42:39 +02:00
Ottermandias
51c7fc83dd Woops. 2024-07-16 23:34:45 +02:00
Ottermandias
a5f35dbd17 Revert legacy tattoo changes. 2024-07-16 23:30:02 +02:00
Ottermandias
c75315f0f7 Update Repo. 2024-07-16 23:01:27 +02:00
Ottermandias
d3824d6a23 Fix some hrothgar gender change remainders. 2024-07-16 22:59:56 +02:00
Ottermandias
303001fed0 Update GameData. 2024-07-16 22:57:37 +02:00
Ottermandias
717f9eb393 Update GameData. 2024-07-16 22:10:35 +02:00
Ottermandias
9529963aa2 Update other things. 2024-07-16 18:19:34 +02:00
Ottermandias
81059411e5 Current state. 2024-07-15 15:19:51 +02:00
Ottermandias
7a602d6ec5 Extricate bonus slots somewhat. 2024-07-11 19:46:01 +02:00
Ottermandias
7caf6cc08a Initial Update for multiple stains, some facewear support, and API X 2024-07-11 14:21:25 +02:00
Ottermandias
c1d9af2dd0 Update Item API. 2024-07-11 14:20:19 +02:00
Ottermandias
3f99d11179 Add support info to Glamourer. 2024-06-23 22:55:31 +02:00
Ottermandias
52b89a4177 Do not apply auto designs to transformed actors maybe? 2024-06-15 11:53:04 +02:00
Ottermandias
5cf6b0eb75 Fix issue with Penumbra load order and visor state. 2024-06-15 11:53:04 +02:00
Actions User
205a95b254 [CI] Updating repo.json for 1.2.3.3 2024-06-11 10:50:56 +00:00
Ottermandias
cd498b539b Ugh. 2024-06-11 12:48:57 +02:00
Ottermandias
089c7d392d Update submodules. 2024-06-11 12:31:02 +02:00
Ottermandias
6207f3a67f Fix an exception. 2024-06-11 12:31:02 +02:00
Actions User
dec3598eff [CI] Updating repo.json for 1.2.3.2 2024-06-07 14:41:01 +00:00
Ottermandias
eb7cc6ffa0 Add IpcPending. 2024-06-05 11:29:04 +02:00
Actions User
960548f7b2 [CI] Updating repo.json for 1.2.3.1 2024-06-01 22:12:12 +00:00
222 changed files with 10447 additions and 4233 deletions

View file

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

View file

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

@ -1 +1 @@
Subproject commit a20d4ab1811a7f66918afab9b41ec723f75054f5
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}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\release.yml = .github\workflows\release.yml
Glamourer\Glamourer.json = Glamourer\Glamourer.json
repo.json = repo.json
.github\workflows\test_release.yml = .github\workflows\test_release.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}"
@ -27,30 +30,30 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|Any CPU
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.Build.0 = Release|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.ActiveCfg = Debug|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.Build.0 = Debug|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.ActiveCfg = Release|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,48 +1,58 @@
using Glamourer.Api.Enums;
using Glamourer.Designs;
using Glamourer.GameData;
using Glamourer.State;
using OtterGui;
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;
using ObjectManager = Glamourer.Interop.ObjectManager;
namespace Glamourer.Api;
public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService
public class ApiHelpers(ActorObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal IEnumerable<ActorState> FindExistingStates(string actorName)
internal IEnumerable<ActorState> FindExistingStates(string actorName, ushort worldId = ushort.MaxValue)
{
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
yield break;
foreach (var state in stateManager.Values.Where(state
=> state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString))
yield return state;
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[objectIndex];
var actor = objects.Objects[objectIndex];
var identifier = actor.GetIdentifier(actors);
if (!identifier.IsValid)
{
state = null;
return GlamourerApiEc.ActorNotFound;
}
stateManager.TryGetValue(identifier, out state);
stateManager.TryGetValue(identifier, out state);
return GlamourerApiEc.Success;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal ActorState? FindState(int objectIndex)
{
var actor = objects[objectIndex];
var actor = objects.Objects[objectIndex];
var identifier = actor.GetIdentifier(actors);
if (identifier.IsValid && stateManager.GetOrCreate(identifier, actor, out var state))
return state;
@ -54,12 +64,10 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM
internal static DesignBase.FlagRestrictionResetter Restrict(DesignBase design, ApplyFlag flags)
=> (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) switch
{
ApplyFlag.Equipment => design.TemporarilyRestrictApplication(EquipFlagExtensions.All, 0, CrestExtensions.All, 0),
ApplyFlag.Customization => design.TemporarilyRestrictApplication(0, CustomizeFlagExtensions.All, 0,
CustomizeParameterExtensions.All),
ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(EquipFlagExtensions.All,
CustomizeFlagExtensions.All, CrestExtensions.All, CustomizeParameterExtensions.All),
_ => design.TemporarilyRestrictApplication(0, 0, 0, 0),
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)]
@ -75,10 +83,8 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM
if (objectName.Length == 0 || !ByteString.FromString(objectName, out var byteString))
return [];
objects.Update();
return stateManager.Values.Where(state => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString)
.Concat(objects.Identifiers
.Concat(objects
.Where(kvp => kvp.Key is { IsValid: true, Type: IdentifierType.Player } && kvp.Key.PlayerName == byteString)
.SelectWhere(kvp =>
{
@ -114,7 +120,10 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM
{
sb.Append(arguments[2 * i]);
sb.Append(" = ");
sb.Append(arguments[2 * i + 1]);
if (arguments[2 * i + 1] is IEnumerable e)
sb.Append($"[{string.Join(',', e)}]");
else
sb.Append(arguments[2 * i + 1]);
sb.Append(", ");
}

View file

@ -2,15 +2,32 @@
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) : IGlamourerApiDesigns, IApiService
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);
@ -33,7 +50,7 @@ public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager
{
var once = (flags & ApplyFlag.Once) != 0;
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true,
ResetMaterials: !once && key != 0);
ResetMaterials: !once && key != 0, IsFinal: true);
using var restrict = ApiHelpers.Restrict(design, flags);
stateManager.ApplyDesign(state, design, settings);
@ -66,4 +83,56 @@ public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager
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

@ -3,14 +3,17 @@ using OtterGui.Services;
namespace Glamourer.Api;
public class GlamourerApi(DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService
public class GlamourerApi(Configuration config, DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService
{
public const int CurrentApiVersionMajor = 1;
public const int CurrentApiVersionMinor = 2;
public const int CurrentApiVersionMinor = 7;
public (int Major, int Minor) ApiVersion
=> (CurrentApiVersionMajor, CurrentApiVersionMinor);
public bool AutoReloadGearEnabled
=> config.AutoRedrawEquipOnChanges;
public IGlamourerApiDesigns Designs
=> designs;

View file

@ -2,6 +2,7 @@ using Dalamud.Plugin;
using Glamourer.Api.Api;
using Glamourer.Api.Helpers;
using OtterGui.Services;
using Glamourer.Api.Enums;
namespace Glamourer.Api;
@ -12,7 +13,7 @@ public sealed class IpcProviders : IDisposable, IApiService
private readonly EventProvider _disposedProvider;
private readonly EventProvider _initializedProvider;
public IpcProviders(DalamudPluginInterface pi, IGlamourerApi api)
public IpcProviders(IDalamudPluginInterface pi, IGlamourerApi api)
{
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
@ -21,29 +22,50 @@ public sealed class IpcProviders : IDisposable, IApiService
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();
@ -59,3 +81,5 @@ public sealed class IpcProviders : IDisposable, IApiService
_disposedProvider.Dispose();
}
}

View file

@ -11,9 +11,9 @@ namespace Glamourer.Api;
public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager stateManager) : IGlamourerApiItems, IApiService
{
public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, byte stain, uint key, ApplyFlag flags)
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, "Stain", stain, "Key", key, "Flags", 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);
@ -27,14 +27,14 @@ public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager
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, stain, settings);
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, byte stain, uint key, ApplyFlag flags)
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, "Stain", stain, "Key", key, "Flags", 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);
@ -53,10 +53,134 @@ public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager
continue;
anyUnlocked = true;
stateManager.ChangeEquip(state, (EquipSlot)slot, item, stain, settings);
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);
@ -79,4 +203,15 @@ public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager
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);
}
}

View file

@ -2,52 +2,59 @@
using Glamourer.Api.Enums;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
using Penumbra.GameData.Interop;
using ObjectManager = Glamourer.Interop.ObjectManager;
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 Configuration _config;
private readonly AutoDesignApplier _autoDesigns;
private readonly ObjectManager _objects;
private readonly StateChanged _stateChanged;
private readonly GPoseService _gPose;
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,
Configuration config,
AutoDesignApplier autoDesigns,
ObjectManager objects,
ActorObjectManager objects,
AutoRedrawChanged autoRedraw,
StateChanged stateChanged,
StateFinalized stateFinalized,
GPoseService gPose)
{
_helpers = helpers;
_stateManager = stateManager;
_converter = converter;
_config = config;
_autoDesigns = autoDesigns;
_objects = objects;
_stateChanged = stateChanged;
_gPose = gPose;
_stateChanged.Subscribe(OnStateChange, Events.StateChanged.Priority.GlamourerIpc);
_gPose.Subscribe(OnGPoseChange, GPoseService.Priority.GlamourerIpc);
_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()
{
_stateChanged.Unsubscribe(OnStateChange);
_autoRedraw.Unsubscribe(OnAutoRedrawChange);
_stateChanged.Unsubscribe(OnStateChanged);
_stateFinalized.Unsubscribe(OnStateFinalized);
_gPose.Unsubscribe(OnGPoseChange);
}
@ -119,6 +126,48 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
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);
@ -176,6 +225,20 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
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);
@ -198,6 +261,27 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
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));
@ -213,7 +297,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
RevertToAutomation(_objects[objectIndex], state, key, flags);
RevertToAutomation(_objects.Objects[objectIndex], state, key, flags);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
@ -247,33 +331,47 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public event Action<nint>? StateChanged;
public event Action<IntPtr, StateChangeType>? StateChangedWithType;
public event Action<bool>? GPoseChanged;
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);
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 & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed;
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);
break;
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);
@ -281,7 +379,6 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
private GlamourerApiEc RevertToAutomation(ActorState state, uint key, ApplyFlag flags)
{
_objects.Update();
if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid)
return GlamourerApiEc.ActorNotFound;
@ -294,8 +391,8 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
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, out var forcedRedraw);
_stateManager.ReapplyState(actor, state, forcedRedraw, source);
_autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, false, out var forcedRedraw);
_stateManager.ReapplyAutomationState(actor, state, forcedRedraw, true, source);
ApiHelpers.Lock(state, key, flags);
}
@ -307,7 +404,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
if (!state.CanUnlock(key))
return (GlamourerApiEc.InvalidKey, null);
return (GlamourerApiEc.Success, _converter.ShareJObject(state, ApplicationRules.AllWithConfig(_config)));
return (GlamourerApiEc.Success, _converter.ShareJObject(state, ApplicationRules.All));
}
private (GlamourerApiEc, string?) ConvertBase64(ActorState? state, uint key)
@ -321,17 +418,18 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
version = DesignConverter.Version;
return state switch
{
string s => _converter.FromBase64(s, (flags & ApplyFlag.Equipment) != 0, (flags & ApplyFlag.Customization) != 0, out version),
JObject j => _converter.FromJObject(j, (flags & ApplyFlag.Equipment) != 0, (flags & ApplyFlag.Customization) != 0),
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 OnGPoseChange(bool gPose)
=> GPoseChanged?.Invoke(gPose);
private void OnAutoRedrawChange(bool autoReload)
=> AutoReloadGearChanged?.Invoke(autoReload);
private void OnStateChange(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, object? _5)
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);
@ -340,4 +438,15 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
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 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."),
];
public static (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat(
this ApplicationType type, IDesignStandIn designStandIn)
public static ApplicationCollection Collection(this ApplicationType type)
{
var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0)
| (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0)
@ -37,16 +37,22 @@ public static class ApplicationTypeExtensions
| (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0);
var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0;
var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0;
var crestFlag = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
var metaFlag = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState : 0)
var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.EarState : 0)
| (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0)
| (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0);
var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0;
if (designStandIn is not DesignBase design)
return (equipFlags, customizeFlags, crestFlag, parameterFlags, metaFlag);
return new ApplicationCollection(equipFlags, bonusFlags, customizeFlags, crestFlags, parameterFlags, metaFlags);
}
return (equipFlags & design!.ApplyEquip, customizeFlags & design.ApplyCustomize, crestFlag & design.ApplyCrest,
parameterFlags & design.ApplyParameters, metaFlag & design.ApplyMeta);
public static ApplicationCollection ApplyWhat(this ApplicationType type, IDesignStandIn designStandIn)
{
if(designStandIn is not DesignBase design)
return type.Collection();
var ret = type.Collection().Restrict(design.Application);
ret.CustomizeRaw = ret.CustomizeRaw.FixApplication(design.CustomizeSet);
return ret;
}
public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;

View file

@ -61,6 +61,6 @@ public class AutoDesign
return ret;
}
public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat()
public ApplicationCollection ApplyWhat()
=> Type.ApplyWhat(Design);
}

View file

@ -11,29 +11,28 @@ using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using ObjectManager = Glamourer.Interop.ObjectManager;
namespace Glamourer.Automation;
public sealed class AutoDesignApplier : IDisposable
{
private readonly Configuration _config;
private readonly AutoDesignManager _manager;
private readonly StateManager _state;
private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset;
private readonly ActorManager _actors;
private readonly AutomationChanged _event;
private readonly ObjectManager _objects;
private readonly WeaponLoading _weapons;
private readonly HumanModelList _humans;
private readonly DesignMerger _designMerger;
private readonly IClientState _clientState;
private readonly Configuration _config;
private readonly AutoDesignManager _manager;
private readonly StateManager _state;
private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset;
private readonly ActorManager _actors;
private readonly AutomationChanged _event;
private readonly ActorObjectManager _objects;
private readonly WeaponLoading _weapons;
private readonly HumanModelList _humans;
private readonly DesignMerger _designMerger;
private readonly IClientState _clientState;
private readonly JobChangeState _jobChangeState;
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)
{
_config = config;
@ -55,6 +54,15 @@ public sealed class AutoDesignApplier : IDisposable
_equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier);
}
public void OnEnableAutoDesignsChanged(bool value)
{
if (value)
return;
foreach (var state in _state.Values)
state.Sources.RemoveFixedDesignSources();
}
public void Dispose()
{
_weapons.Unsubscribe(OnWeaponLoading);
@ -77,7 +85,7 @@ public sealed class AutoDesignApplier : IDisposable
{
case EquipSlot.MainHand:
{
if (_jobChangeState.TryGetValue(current.Type, actor.Job, out var data))
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{
Glamourer.Log.Verbose(
$"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}.");
@ -89,7 +97,7 @@ public sealed class AutoDesignApplier : IDisposable
}
case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand():
{
if (_jobChangeState.TryGetValue(current.Type, actor.Job, out var data))
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{
Glamourer.Log.Verbose(
$"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}.");
@ -145,16 +153,15 @@ public sealed class AutoDesignApplier : IDisposable
if (newSet is not { Enabled: true })
return;
_objects.Update();
foreach (var id in newSet.Identifiers)
{
if (_objects.TryGetValue(id, out var data))
{
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, out var forcedRedraw);
Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
foreach (var actor in data.Objects)
_state.ReapplyState(actor, forcedRedraw,StateSource.Fixed);
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
}
}
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
@ -164,8 +171,8 @@ public sealed class AutoDesignApplier : IDisposable
var specificId = actor.GetIdentifier(_actors);
if (_state.GetOrCreate(specificId, actor, out var state))
{
Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, out var forcedRedraw);
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
}
}
}
@ -212,22 +219,21 @@ public sealed class AutoDesignApplier : IDisposable
var respectManual = state.LastJob == newJob.Id;
state.LastJob = actor.Job;
Reduce(actor, state, set, respectManual, true, out var forcedRedraw);
Reduce(actor, state, set, respectManual, true, true, out var forcedRedraw);
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
}
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, out bool forcedRedraw)
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, bool forcedNew, out bool forcedRedraw)
{
forcedRedraw = false;
if (!_config.EnableAutoDesigns)
return;
if (!GetPlayerSet(identifier, out var set))
return;
if (reset)
_state.ResetState(state, StateSource.Game);
Reduce(actor, state, set, false, false, out forcedRedraw);
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)
@ -235,9 +241,6 @@ public sealed class AutoDesignApplier : IDisposable
AutoDesignSet set;
if (!_state.TryGetValue(identifier, out state))
{
if (!_config.EnableAutoDesigns)
return false;
if (!GetPlayerSet(identifier, out set!))
return false;
@ -254,11 +257,12 @@ public sealed class AutoDesignApplier : IDisposable
var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange;
if (!respectManual)
_state.ResetState(state, StateSource.Game);
Reduce(actor, state, set, respectManual, false, out _);
Reduce(actor, state, set, respectManual, false, false, out _);
return true;
}
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange, out bool forcedRedraw)
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)
{
@ -277,12 +281,27 @@ public sealed class AutoDesignApplier : IDisposable
}
forcedRedraw = false;
if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId))
if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId))
return;
if (actor.IsTransformed)
return;
var mergedDesign = _designMerger.Merge(
set.Designs.Where(d => d.IsActive(actor)).SelectMany(d => d.Design.AllLinks.Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))),
set.Designs.Where(d => d.IsActive(actor))
.SelectMany(d => d.Design.AllLinks(newApplication).Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))),
state.ModelData.Customize, state.BaseData, true, _config.AlwaysApplyAssociatedMods);
if (_objects.IsInGPose && actor.IsGPoseOrCutscene)
{
mergedDesign.ResetTemporarySettings = false;
mergedDesign.AssociatedMods.Clear();
}
else if (set.ResetTemporarySettings)
{
mergedDesign.ResetTemporarySettings = true;
}
_state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false, false, false));
forcedRedraw = mergedDesign.ForcedRedraw;
}
@ -290,18 +309,31 @@ public sealed class AutoDesignApplier : IDisposable
/// <summary> Get world-specific first and all-world afterward. </summary>
private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set)
{
if (!_config.EnableAutoDesigns)
{
set = null;
return false;
}
switch (identifier.Type)
{
case IdentifierType.Player:
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreatePlayer(identifier.PlayerName, ushort.MaxValue);
identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld);
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Retainer:
case IdentifierType.Npc:
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Owned:
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId);
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
return _manager.EnabledSets.TryGetValue(identifier, out set);
default:
@ -326,7 +358,7 @@ public sealed class AutoDesignApplier : IDisposable
var respectManual = prior == id;
NewGearsetId = id;
Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, out var forcedRedraw);
Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, prior == id, out var forcedRedraw);
NewGearsetId = -1;
foreach (var actor in data.Objects)
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);

View file

@ -1,6 +1,7 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Designs.Special;
using Glamourer.Events;
using Glamourer.Interop;
@ -9,6 +10,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Filesystem;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
@ -233,6 +235,22 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
_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)
{
var newDesign = new AutoDesign()
@ -443,8 +461,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
var set = new AutoDesignSet(name, group)
{
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current,
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
ResetTemporarySettings = obj["ResetTemporarySettings"]?.ToObject<bool>() ?? false,
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current,
};
if (set.Enabled)
@ -569,12 +588,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
IdentifierType.Player => true,
IdentifierType.Retainer => true,
IdentifierType.Npc => true,
IdentifierType.Owned => true,
_ => false,
};
if (!validType)
{
group = Array.Empty<ActorIdentifier>();
group = [];
return false;
}
@ -600,8 +620,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
? ActorIdentifier.RetainerType.Mannequin
: 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)
@ -615,12 +636,11 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
};
return table.Where(kvp => kvp.Value == name)
.Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id,
identifier.Kind,
kvp.Key)).ToArray();
identifier.Kind, kvp.Key)).ToArray();
}
}
private void OnDesignChange(DesignChanged.Type type, Design design, object? data)
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _)
{
if (type is not DesignChanged.Type.Deleted)
return;

View file

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

View file

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

View file

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

View file

@ -0,0 +1,96 @@
using Glamourer.Designs;
using Dalamud.Bindings.ImGui;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
namespace Glamourer;
[Flags]
public enum DesignPanelFlag : uint
{
Customization = 0x0001,
Equipment = 0x0002,
AdvancedCustomizations = 0x0004,
AdvancedDyes = 0x0008,
AppearanceDetails = 0x0010,
DesignDetails = 0x0020,
ModAssociations = 0x0040,
DesignLinks = 0x0080,
ApplicationRules = 0x0100,
DebugData = 0x0200,
}
public static class DesignPanelFlagExtensions
{
public static ReadOnlySpan<byte> ToName(this DesignPanelFlag flag)
=> flag switch
{
DesignPanelFlag.Customization => "Customization"u8,
DesignPanelFlag.Equipment => "Equipment"u8,
DesignPanelFlag.AdvancedCustomizations => "Advanced Customization"u8,
DesignPanelFlag.AdvancedDyes => "Advanced Dyes"u8,
DesignPanelFlag.DesignDetails => "Design Details"u8,
DesignPanelFlag.ApplicationRules => "Application Rules"u8,
DesignPanelFlag.ModAssociations => "Mod Associations"u8,
DesignPanelFlag.DesignLinks => "Design Links"u8,
DesignPanelFlag.DebugData => "Debug Data"u8,
DesignPanelFlag.AppearanceDetails => "Appearance Details"u8,
_ => ""u8,
};
public static CollapsingHeader Header(this DesignPanelFlag flag, Configuration config)
{
if (config.HideDesignPanel.HasFlag(flag))
return new CollapsingHeader()
{
Disposed = true,
};
var expand = config.AutoExpandDesignPanel.HasFlag(flag);
return ImUtf8.CollapsingHeaderId(flag.ToName(), expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
}
public static void DrawTable(ReadOnlySpan<byte> label, DesignPanelFlag hidden, DesignPanelFlag expanded, Action<DesignPanelFlag> setterHide,
Action<DesignPanelFlag> setterExpand)
{
var checkBoxWidth = Math.Max(ImGui.GetFrameHeight(), ImUtf8.CalcTextSize("Expand"u8).X);
var textWidth = ImUtf8.CalcTextSize(DesignPanelFlag.AdvancedCustomizations.ToName()).X;
var tableSize = 2 * (textWidth + 2 * checkBoxWidth) + 10 * ImGui.GetStyle().CellPadding.X + 2 * ImGui.GetStyle().WindowPadding.X + 2 * ImGui.GetStyle().FrameBorderSize;
using var table = ImUtf8.Table(label, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders, new Vector2(tableSize, 6 * ImGui.GetFrameHeight()));
if (!table)
return;
var headerColor = ImGui.GetColorU32(ImGuiCol.TableHeaderBg);
var checkBoxOffset = (checkBoxWidth - ImGui.GetFrameHeight()) / 2;
ImUtf8.TableSetupColumn("Panel##1"u8, ImGuiTableColumnFlags.WidthFixed, textWidth);
ImUtf8.TableSetupColumn("Show##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImUtf8.TableSetupColumn("Expand##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImUtf8.TableSetupColumn("Panel##2"u8, ImGuiTableColumnFlags.WidthFixed, textWidth);
ImUtf8.TableSetupColumn("Show##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImUtf8.TableSetupColumn("Expand##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImGui.TableHeadersRow();
foreach (var panel in Enum.GetValues<DesignPanelFlag>())
{
using var id = ImUtf8.PushId((int)panel);
ImGui.TableNextColumn();
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, headerColor);
ImUtf8.TextFrameAligned(panel.ToName());
var isShown = !hidden.HasFlag(panel);
var isExpanded = expanded.HasFlag(panel);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset);
if (ImUtf8.Checkbox("##show"u8, ref isShown))
setterHide.Invoke(isShown ? hidden & ~panel : hidden | panel);
ImUtf8.HoverTooltip(
"Show this panel and associated functionality in all relevant tabs.\n\nToggling this off does NOT disable any functionality, just the display of it, so hide panels at your own risk."u8);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset);
if (ImUtf8.Checkbox("##expand"u8, ref isExpanded))
setterExpand.Invoke(isExpanded ? expanded | panel : expanded & ~panel);
ImUtf8.HoverTooltip("Expand this panel by default in all relevant tabs."u8);
}
}
}

View file

@ -0,0 +1,68 @@
using Glamourer.Api.Enums;
using Glamourer.GameData;
using Dalamud.Bindings.ImGui;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public record struct ApplicationCollection(
EquipFlag Equip,
BonusItemFlag BonusItem,
CustomizeFlag CustomizeRaw,
CrestFlag Crest,
CustomizeParameterFlag Parameters,
MetaFlag Meta)
{
public static readonly ApplicationCollection All = new(EquipFlagExtensions.All, BonusExtensions.All,
CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All);
public static readonly ApplicationCollection None = new(0, 0, CustomizeFlag.BodyType, 0, 0, 0);
public static readonly ApplicationCollection Equipment = new(EquipFlagExtensions.All, BonusExtensions.All,
CustomizeFlag.BodyType, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState | MetaFlag.EarState);
public static readonly ApplicationCollection Customizations = new(0, 0, CustomizeFlagExtensions.AllRelevant, 0,
CustomizeParameterExtensions.All, MetaFlag.Wetness);
public static readonly ApplicationCollection Default = new(EquipFlagExtensions.All, BonusExtensions.All,
CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
public static ApplicationCollection FromKeys()
=> (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
{
(false, false) => All,
(true, true) => All,
(true, false) => Equipment,
(false, true) => Customizations,
};
public CustomizeFlag Customize
{
get => CustomizeRaw;
set => CustomizeRaw = value | CustomizeFlag.BodyType;
}
public void RemoveEquip()
{
Equip = 0;
BonusItem = 0;
Crest = 0;
Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
}
public void RemoveCustomize()
{
Customize = 0;
Parameters = 0;
Meta &= MetaFlag.Wetness;
}
public ApplicationCollection Restrict(ApplicationCollection old)
=> new(old.Equip & Equip, old.BonusItem & BonusItem, (old.Customize & Customize) | CustomizeFlag.BodyType, old.Crest & Crest,
old.Parameters & Parameters, old.Meta & Meta);
public ApplicationCollection CloneSecure()
=> new(Equip & EquipFlagExtensions.All, BonusItem & BonusExtensions.All,
(Customize & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType, Crest & CrestExtensions.AllRelevant,
Parameters & CustomizeParameterExtensions.All, Meta & MetaExtensions.All);
}

View file

@ -1,20 +1,14 @@
using Glamourer.GameData;
using Glamourer.Api.Enums;
using Glamourer.GameData;
using Glamourer.State;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public readonly struct ApplicationRules(
EquipFlag equip,
CustomizeFlag customize,
CrestFlag crest,
CustomizeParameterFlag parameters,
MetaFlag meta,
bool materials)
public readonly struct ApplicationRules(ApplicationCollection application, bool materials)
{
public static readonly ApplicationRules All = new(EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant,
CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All, true);
public static readonly ApplicationRules All = new(ApplicationCollection.All, true);
public static ApplicationRules FromModifiers(ActorState state)
=> FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
@ -23,54 +17,40 @@ public readonly struct ApplicationRules(
=> NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
public static ApplicationRules AllButParameters(ActorState state)
=> new(All.Equip, All.Customize, All.Crest, ComputeParameters(state.ModelData, state.BaseData, All.Parameters), All.Meta, true);
public static ApplicationRules AllWithConfig(Configuration config)
=> new(All.Equip, All.Customize, All.Crest, config.UseAdvancedParameters ? All.Parameters : 0, All.Meta, config.UseAdvancedDyes);
=> new(ApplicationCollection.All with { Parameters = ComputeParameters(state.ModelData, state.BaseData, All.Parameters) }, true);
public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift)
=> new(ctrl || !shift ? EquipFlagExtensions.All : 0,
!ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0,
0,
0,
ctrl || !shift ? MetaFlag.VisorState : 0, false);
{
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
var visor = equip != 0 ? MetaFlag.VisorState : 0;
return new ApplicationRules(new ApplicationCollection(equip, 0, customize, 0, 0, visor), false);
}
public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift)
{
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
var bonus = equip == 0 ? 0 : BonusExtensions.All;
var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant;
var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All;
var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0;
if (equip != 0)
meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState;
return new ApplicationRules(equip, customize, crest, ComputeParameters(state.ModelData, state.BaseData, parameters), meta, equip != 0);
var collection = new ApplicationCollection(equip, bonus, customize, crest,
ComputeParameters(state.ModelData, state.BaseData, parameters), meta);
return new ApplicationRules(collection, equip != 0);
}
public void Apply(DesignBase design)
{
design.ApplyEquip = Equip;
design.ApplyCustomize = Customize;
design.ApplyCrest = Crest;
design.ApplyParameters = Parameters;
design.ApplyMeta = Meta;
}
=> design.Application = application;
public EquipFlag Equip
=> equip & EquipFlagExtensions.All;
public CustomizeFlag Customize
=> customize & CustomizeFlagExtensions.AllRelevant;
public CrestFlag Crest
=> crest & CrestExtensions.AllRelevant;
=> application.Equip & EquipFlagExtensions.All;
public CustomizeParameterFlag Parameters
=> parameters & CustomizeParameterExtensions.All;
public MetaFlag Meta
=> meta & MetaExtensions.All;
=> application.Parameters & CustomizeParameterExtensions.All;
public bool Materials
=> materials;

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Glamourer.Automation;
using Glamourer.Designs.Links;
using Glamourer.Interop.Material;
@ -9,6 +9,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Structs;
using Notification = OtterGui.Classes.Notification;
namespace Glamourer.Designs;
@ -27,32 +28,39 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
internal Design(Design other)
: base(other)
{
Tags = [.. other.Tags];
Description = other.Description;
QuickDesign = other.QuickDesign;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
Tags = [.. other.Tags];
Description = other.Description;
QuickDesign = other.QuickDesign;
ForcedRedraw = other.ForcedRedraw;
ResetAdvancedDyes = other.ResetAdvancedDyes;
ResetTemporarySettings = other.ResetTemporarySettings;
Color = other.Color;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
Links = Links.Clone();
}
// Metadata
public new const int FileVersion = 1;
public new const int FileVersion = 2;
public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; }
public DateTimeOffset LastEdit { get; internal set; }
public LowerString Name { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = [];
public int Index { get; internal set; }
public bool ForcedRedraw { get; internal set; }
public bool QuickDesign { get; internal set; } = true;
public string Color { get; internal set; } = string.Empty;
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = [];
public LinkContainer Links { get; private set; } = [];
public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; }
public DateTimeOffset LastEdit { get; internal set; }
public LowerString Name { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = [];
public int Index { get; internal set; }
public bool ForcedRedraw { get; internal set; }
public bool ResetAdvancedDyes { get; internal set; }
public bool ResetTemporarySettings { get; internal set; }
public bool QuickDesign { get; internal set; } = true;
public string Color { get; internal set; } = string.Empty;
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = [];
public LinkContainer Links { get; private set; } = [];
public string Incognito
=> Identifier.ToString()[..8];
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
=> LinkContainer.GetAllLinks(this).Select(t => ((IDesignStandIn)t.Link.Link, t.Link.Type, JobFlag.All));
#endregion
@ -92,25 +100,28 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
public new JObject JsonSerialize()
{
var ret = new JObject()
var ret = new JObject
{
["FileVersion"] = FileVersion,
["Identifier"] = Identifier,
["CreationDate"] = CreationDate,
["LastEdit"] = LastEdit,
["Name"] = Name.Text,
["Description"] = Description,
["ForcedRedraw"] = ForcedRedraw,
["Color"] = Color,
["QuickDesign"] = QuickDesign,
["Tags"] = JArray.FromObject(Tags),
["WriteProtected"] = WriteProtected(),
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
["Mods"] = SerializeMods(),
["Links"] = Links.Serialize(),
["FileVersion"] = FileVersion,
["Identifier"] = Identifier,
["CreationDate"] = CreationDate,
["LastEdit"] = LastEdit,
["Name"] = Name.Text,
["Description"] = Description,
["ForcedRedraw"] = ForcedRedraw,
["ResetAdvancedDyes"] = ResetAdvancedDyes,
["ResetTemporarySettings"] = ResetTemporarySettings,
["Color"] = Color,
["QuickDesign"] = QuickDesign,
["Tags"] = JArray.FromObject(Tags),
["WriteProtected"] = WriteProtected(),
["Equipment"] = SerializeEquipment(),
["Bonus"] = SerializeBonusItems(),
["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
["Mods"] = SerializeMods(),
["Links"] = Links.Serialize(),
};
return ret;
}
@ -120,12 +131,17 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
var ret = new JArray();
foreach (var (mod, settings) in AssociatedMods)
{
var obj = new JObject()
var obj = new JObject
{
["Name"] = mod.Name,
["Directory"] = mod.DirectoryName,
["Enabled"] = settings.Enabled,
};
if (settings.Remove)
obj["Remove"] = true;
else if (settings.ForceInherit)
obj["Inherit"] = true;
else
obj["Enabled"] = settings.Enabled;
if (settings.Enabled)
{
obj["Priority"] = settings.Priority;
@ -142,17 +158,83 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
#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;
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."),
};
}
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");
@ -171,17 +253,20 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
design.SetWriteProtected(json["WriteProtected"]?.ToObject<bool>() ?? false);
LoadCustomize(customizations, json["Customize"], design, design.Name, true, false);
LoadEquip(items, json["Equipment"], design, design.Name, true);
LoadBonus(items, design, json["Bonus"]);
LoadMods(json["Mods"], design);
LoadParameters(json["Parameters"], design, design.Name);
LoadMaterials(json["Materials"], design, design.Name);
LoadLinks(linkLoader, json["Links"], design);
design.Color = json["Color"]?.ToObject<string>() ?? string.Empty;
design.ForcedRedraw = json["ForcedRedraw"]?.ToObject<bool>() ?? false;
design.Color = json["Color"]?.ToObject<string>() ?? string.Empty;
design.ForcedRedraw = json["ForcedRedraw"]?.ToObject<bool>() ?? false;
design.ResetAdvancedDyes = json["ResetAdvancedDyes"]?.ToObject<bool>() ?? false;
design.ResetTemporarySettings = json["ResetTemporarySettings"]?.ToObject<bool>() ?? false;
return design;
static string[] ParseTags(JObject json)
{
var tags = json["Tags"]?.ToObject<string[]>() ?? Array.Empty<string>();
var tags = json["Tags"]?.ToObject<string[]>() ?? [];
return tags.OrderBy(t => t).Distinct().ToArray();
}
}
@ -202,12 +287,15 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
continue;
}
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, List<string>>>() ?? [];
var settings = new Dictionary<string, List<string>>(settingsDict.Count);
var forceInherit = tok["Inherit"]?.ToObject<bool>() ?? false;
var removeSetting = tok["Remove"]?.ToObject<bool>() ?? false;
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, List<string>>>() ?? [];
var settings = new Dictionary<string, List<string>>(settingsDict.Count);
foreach (var (key, value) in settingsDict)
settings.Add(key, value);
var priority = tok["Priority"]?.ToObject<int>() ?? 0;
if (!design.AssociatedMods.TryAdd(new Mod(name, directory), new ModSettings(settings, priority, enabled.Value)))
if (!design.AssociatedMods.TryAdd(new Mod(name, directory),
new ModSettings(settings, priority, enabled.Value, forceInherit, removeSetting)))
Glamourer.Messager.NotificationMessage("The loaded design contains a mod more than once, skipped.", NotificationType.Warning);
}
}
@ -226,10 +314,10 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
if (array == null)
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 type = (ApplicationType)(obj["Type"]?.ToObject<uint>() ?? 0);
var identifier = jObj["Design"]?.ToObject<Guid>() ?? throw new ArgumentNullException(nameof(design));
var type = (ApplicationType)(jObj["Type"]?.ToObject<uint>() ?? 0);
linkLoader.AddObject(design, new LinkData(identifier, type, order));
}
}

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignConverter(
SaveService saveService,
ItemManager _items,
DesignManager _designs,
CustomizeService _customize,
@ -69,19 +70,14 @@ public class DesignConverter(
try
{
var ret = jObject["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObject)
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObject)
: DesignBase.LoadDesignBase(_customize, _items, jObject);
ret.SetApplyMeta(MetaIndex.Wetness, customize);
if (!customize)
ret.ApplyCustomize = 0;
ret.Application.RemoveCustomize();
if (!equip)
{
ret.ApplyEquip = 0;
ret.ApplyCrest = 0;
ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState);
}
ret.Application.RemoveEquip();
return ret;
}
@ -105,7 +101,7 @@ public class DesignConverter(
case (byte)'{':
var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes));
ret = jObj1["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj1)
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj1)
: DesignBase.LoadDesignBase(_customize, _items, jObj1);
break;
case 1:
@ -120,7 +116,7 @@ public class DesignConverter(
var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 3);
ret = jObj2["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj2)
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
}
@ -131,7 +127,7 @@ public class DesignConverter(
var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 5);
ret = jObj2["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj2)
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
}
@ -141,7 +137,7 @@ public class DesignConverter(
var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 6);
ret = jObj2["Identifier"] != null
? Design.LoadDesign(_customize, _items, _linkLoader, jObj2)
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
}
@ -155,16 +151,11 @@ public class DesignConverter(
return null;
}
ret.SetApplyMeta(MetaIndex.Wetness, customize);
if (!customize)
ret.ApplyCustomize = 0;
ret.Application.RemoveCustomize();
if (!equip)
{
ret.ApplyEquip = 0;
ret.ApplyCrest = 0;
ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState);
}
ret.Application.RemoveEquip();
return ret;
}
@ -176,7 +167,7 @@ public class DesignConverter(
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)
{
if (armors.Count != 10)
@ -194,7 +185,7 @@ public class DesignConverter(
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);
@ -204,7 +195,7 @@ public class DesignConverter(
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);
if (!skipWarnings && !oh.Valid)
@ -215,29 +206,47 @@ public class DesignConverter(
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,
EquipFlag equipFlags = EquipFlagExtensions.All)
EquipFlag equipFlags = EquipFlagExtensions.All, BonusItemFlag bonusFlags = BonusExtensions.All)
{
foreach (var (key, value) in materials.Values)
{
var idx = MaterialValueIndex.FromKey(key);
if (idx.RowIndex >= LegacyColorTable.NumUsedRows)
if (idx.RowIndex >= ColorTable.NumRows)
continue;
if (idx.MaterialIndex >= MaterialService.MaterialsPerModel)
continue;
var slot = idx.DrawObject switch
switch (idx.DrawObject)
{
MaterialValueIndex.DrawObjectType.Human => idx.SlotIndex < 10 ? ((uint)idx.SlotIndex).ToEquipSlot() : EquipSlot.Unknown,
MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0 => EquipSlot.MainHand,
MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0 => EquipSlot.OffHand,
_ => EquipSlot.Unknown,
};
if (slot is EquipSlot.Unknown || (slot.ToBothFlags() & equipFlags) == 0)
continue;
case MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0:
if ((equipFlags & (EquipFlag.Mainhand | EquipFlag.MainhandStain)) == 0)
continue;
break;
case MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0:
if ((equipFlags & (EquipFlag.Offhand | EquipFlag.OffhandStain)) == 0)
continue;
break;
case MaterialValueIndex.DrawObjectType.Human:
if (idx.SlotIndex < 10)
{
if ((((uint)idx.SlotIndex).ToEquipSlot().ToBothFlags() & equipFlags) == 0)
continue;
}
else if (idx.SlotIndex >= 16)
{
if (((idx.SlotIndex - 16u).ToBonusSlot() & bonusFlags) == 0)
continue;
}
break;
default: continue;
}
manager.AddOrUpdateValue(idx, value.Convert());
}

View file

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

View file

@ -1,9 +1,9 @@
using Glamourer.Designs.History;
using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -73,7 +73,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx));
DesignChanged.Invoke(DesignChanged.Type.Customize, design, new CustomizeTransaction(idx, oldValue, value));
}
/// <inheritdoc/>
@ -89,7 +89,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}.");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, (oldCustomize, applied, changed));
DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, new EntireCustomizeTransaction(changed, oldCustomize, newCustomize));
}
/// <inheritdoc/>
@ -104,7 +104,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}.");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Parameter, design, (old, @new, flag));
DesignChanged.Invoke(DesignChanged.Type.Parameter, design, new ParameterTransaction(flag, old, @new));
}
/// <inheritdoc/>
@ -123,11 +123,14 @@ public class DesignEditor(
if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets))
return;
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId}).");
DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff, newGauntlets));
DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
new WeaponTransaction(currentMain, currentOff, currentGauntlets, item, newOff ?? currentOff,
newGauntlets ?? currentGauntlets));
return;
}
case EquipSlot.OffHand:
@ -140,11 +143,13 @@ public class DesignEditor(
if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item))
return;
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId}).");
DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item));
DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
new WeaponTransaction(currentMain, currentOff, currentGauntlets, currentMain, item, currentGauntlets));
return;
}
default:
@ -160,36 +165,53 @@ public class DesignEditor(
Glamourer.Log.Debug(
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}).");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Equip, design, (old, item, slot));
DesignChanged.Invoke(DesignChanged.Type.Equip, design, new EquipTransaction(slot, old, item));
return;
}
}
}
/// <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;
if (Items.ValidateStain(stain, out var _, false).Length > 0)
if (item.Type.ToBonus() != slot)
return;
var oldStain = design.DesignData.Stain(slot);
if (!design.GetDesignDataRef().SetStain(slot, stain))
var oldItem = design.DesignData.BonusItem(slot);
if (!design.GetDesignDataRef().SetBonusItem(slot, item))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}.");
DesignChanged.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot));
Glamourer.Log.Debug($"Set {slot} bonus item to {item}.");
DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, new BonusItemTransaction(slot, oldItem, item));
}
/// <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)
ChangeItem(data, slot, item.Value, _);
if (stain.HasValue)
ChangeStain(data, slot, stain.Value, _);
if (stains.HasValue)
ChangeStains(data, slot, stains.Value, _);
}
/// <inheritdoc/>
@ -203,7 +225,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}.");
DesignChanged.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot));
DesignChanged.Invoke(DesignChanged.Type.Crest, design, new CrestTransaction(slot, oldCrest, crest));
}
/// <inheritdoc/>
@ -216,7 +238,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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)
@ -229,7 +251,7 @@ public class DesignEditor(
Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}");
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, index);
DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert));
}
public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row)
@ -264,7 +286,7 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow;
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)
@ -277,13 +299,13 @@ public class DesignEditor(
Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}.");
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, index);
DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, new ApplicationTransaction(index, !value, value));
}
/// <inheritdoc/>
public void ApplyDesign(object data, MergedDesign other, ApplySettings _ = default)
=> ApplyDesign(data, other.Design);
public void ApplyDesign(object data, MergedDesign other, ApplySettings settings = default)
=> ApplyDesign(data, other.Design, settings);
/// <inheritdoc/>
public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default)
@ -308,6 +330,12 @@ public class DesignEditor(
_forceFullItemOff = false;
foreach (var slot in BonusExtensions.AllFlags)
{
if (other.DoApplyBonusItem(slot))
ChangeBonusItem(design, slot, other.DesignData.BonusItem(slot));
}
foreach (var slot in Enum.GetValues<CrestFlag>().Where(other.DoApplyCrest))
ChangeCrest(design, slot, other.DesignData.Crest(slot));
@ -338,7 +366,7 @@ public class DesignEditor(
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);
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.Services;
using Newtonsoft.Json;
@ -40,11 +41,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct CreationDate : ISortMode<Design>
{
public string Name
=> "Creation Date (Older First)";
public ReadOnlySpan<byte> Name
=> "Creation Date (Older First)"u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date.";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."u8;
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate));
@ -52,11 +53,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct UpdateDate : ISortMode<Design>
{
public string Name
=> "Update Date (Older First)";
public ReadOnlySpan<byte> Name
=> "Update Date (Older First)"u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date.";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."u8;
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit));
@ -64,11 +65,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct InverseCreationDate : ISortMode<Design>
{
public string Name
=> "Creation Date (Newer First)";
public ReadOnlySpan<byte> Name
=> "Creation Date (Newer First)"u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date.";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."u8;
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate));
@ -76,11 +77,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct InverseUpdateDate : ISortMode<Design>
{
public string Name
=> "Update Date (Newer First)";
public ReadOnlySpan<byte> Name
=> "Update Date (Newer First)"u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date.";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."u8;
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit));
@ -92,13 +93,13 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
_saveService.QueueSave(this);
}
private void OnDesignChange(DesignChanged.Type type, Design design, object? data)
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? data)
{
switch (type)
{
case DesignChanged.Type.Created:
var parent = Root;
if (data is string path)
if ((data as CreationTransaction?)?.Path is { } path)
try
{
parent = FindOrCreateAllFolders(path);
@ -113,14 +114,14 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
return;
case DesignChanged.Type.Deleted:
if (FindLeaf(design, out var leaf1))
if (TryGetValue(design, out var leaf1))
Delete(leaf1);
return;
case DesignChanged.Type.ReloadedAll:
Reload();
return;
case DesignChanged.Type.Renamed when data is string oldName:
if (!FindLeaf(design, out var leaf2))
case DesignChanged.Type.Renamed when (data as RenameTransaction?)?.Old is { } oldName:
if (!TryGetValue(design, out var leaf2))
return;
var old = oldName.FixName();
@ -149,15 +150,6 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
? (string.Empty, false)
: (DesignToIdentifier(design), true);
// Search the entire filesystem for the leaf corresponding to a design.
public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf)
{
leaf = Root.GetAllDescendants(ISortMode<Design>.Lexicographical)
.OfType<Leaf>()
.FirstOrDefault(l => l.Value == design);
return leaf != null;
}
internal static void MigrateOldPaths(SaveService saveService, Dictionary<string, string> oldPaths)
{
if (oldPaths.Count == 0)

View file

@ -1,15 +1,18 @@
using Dalamud.Utility;
using Glamourer.Designs.History;
using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using OtterGui.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public sealed class DesignManager : DesignEditor
@ -52,7 +55,7 @@ public sealed class DesignManager : DesignEditor
{
var text = File.ReadAllText(f.FullName);
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));
}
catch (Exception ex)
@ -98,16 +101,21 @@ public sealed class DesignManager : DesignEditor
var (actualName, path) = ParseName(name, handlePath);
var design = new Design(Customizations, Items)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = Designs.Count,
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = Designs.Count,
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
};
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, path);
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
return design;
}
@ -117,17 +125,22 @@ public sealed class DesignManager : DesignEditor
var (actualName, path) = ParseName(name, handlePath);
var design = new Design(clone)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = Designs.Count,
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = Designs.Count,
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
};
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design.");
SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, path);
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
return design;
}
@ -143,11 +156,12 @@ public sealed class DesignManager : DesignEditor
Name = actualName,
Index = Designs.Count,
};
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design);
Glamourer.Log.Debug(
$"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}.");
SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, path);
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
return design;
}
@ -176,7 +190,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -190,7 +204,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -204,7 +218,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -215,10 +229,10 @@ public sealed class DesignManager : DesignEditor
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
design.LastEdit = DateTimeOffset.UtcNow;
var idx = design.Tags.IndexOf(tag);
var idx = design.Tags.AsEnumerable().IndexOf(tag);
SaveService.QueueSave(design);
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>
@ -232,7 +246,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -247,7 +261,8 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -259,7 +274,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -271,17 +286,26 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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));
}
/// <summary> Add or update an associated mod to a design. </summary>
public void UpdateMod(Design design, Mod mod, ModSettings settings)
{
var hasOldSettings = design.AssociatedMods.TryGetValue(mod, out var oldSettings);
design.AssociatedMods[mod] = settings;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Updated (or added) associated mod {mod.DirectoryName} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings));
if (hasOldSettings)
{
Glamourer.Log.Debug($"Updated associated mod {mod.DirectoryName} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.UpdatedMod, design, new ModUpdatedTransaction(mod, oldSettings, settings));
}
else
{
Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings));
}
}
/// <summary> Set the write protection status of a design. </summary>
@ -292,7 +316,7 @@ public sealed class DesignManager : DesignEditor
SaveService.QueueSave(design);
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>
@ -305,7 +329,7 @@ public sealed class DesignManager : DesignEditor
SaveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar.");
DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, value);
DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, null);
}
#endregion
@ -319,10 +343,32 @@ public sealed class DesignManager : DesignEditor
design.ForcedRedraw = forcedRedraw;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set {design.Identifier} to {(forcedRedraw ? "not" : string.Empty)} force redraws.");
Glamourer.Log.Debug($"Set {design.Identifier} to {(forcedRedraw ? string.Empty : "not")} force redraws.");
DesignChanged.Invoke(DesignChanged.Type.ForceRedraw, design, null);
}
public void ChangeResetAdvancedDyes(Design design, bool resetAdvancedDyes)
{
if (design.ResetAdvancedDyes == resetAdvancedDyes)
return;
design.ResetAdvancedDyes = resetAdvancedDyes;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set {design.Identifier} to {(resetAdvancedDyes ? string.Empty : "not")} reset advanced dyes.");
DesignChanged.Invoke(DesignChanged.Type.ResetAdvancedDyes, design, null);
}
public void ChangeResetTemporarySettings(Design design, bool resetTemporarySettings)
{
if (design.ResetTemporarySettings == resetTemporarySettings)
return;
design.ResetTemporarySettings = resetTemporarySettings;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set {design.Identifier} to {(resetTemporarySettings ? string.Empty : "not")} reset temporary settings.");
DesignChanged.Invoke(DesignChanged.Type.ResetTemporarySettings, design, null);
}
/// <summary> Change whether to apply a specific customize value. </summary>
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
{
@ -332,7 +378,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -344,11 +390,23 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
public void ChangeApplyStains(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyStain(slot, value))
return;
@ -356,7 +414,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -368,7 +426,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -380,7 +438,7 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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>
@ -392,7 +450,40 @@ public sealed class DesignManager : DesignEditor
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
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
@ -466,7 +557,7 @@ public sealed class DesignManager : DesignEditor
try
{
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.");
}
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 UseSingleSource = false,
bool MergeLinks = false,
bool ResetMaterials = false)
bool ResetMaterials = false,
bool IsFinal = false)
{
public static readonly ApplySettings Manual = new()
{
@ -24,6 +25,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false,
MergeLinks = false,
ResetMaterials = false,
IsFinal = false,
};
public static readonly ApplySettings ManualWithLinks = new()
@ -35,6 +37,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false,
MergeLinks = true,
ResetMaterials = false,
IsFinal = false,
};
public static readonly ApplySettings Game = new()
@ -46,6 +49,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false,
MergeLinks = false,
ResetMaterials = true,
IsFinal = false,
};
}
@ -64,12 +68,15 @@ public interface IDesignEditor
public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default)
=> ChangeEquip(data, slot, item, null, settings);
/// <summary> Change a bonus item. </summary>
public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default);
/// <summary> Change the stain for any equipment piece. </summary>
public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings settings = default)
=> ChangeEquip(data, slot, null, stain, settings);
public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings = default)
=> ChangeEquip(data, slot, null, stains, settings);
/// <summary> Change an equipment piece and its stain at the same time. </summary>
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, 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>
public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default);

View file

@ -16,7 +16,7 @@ public interface IDesignStandIn : IEquatable<IDesignStandIn>
public string SerializeName();
public StateSource AssociatedSource();
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks { get; }
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication);
public void AddData(JObject jObj);
@ -25,4 +25,7 @@ public interface IDesignStandIn : IEquatable<IDesignStandIn>
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 OtterGui;
using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Notification = OtterGui.Classes.Notification;
namespace Glamourer.Designs.Links;

View file

@ -1,4 +1,5 @@
using Glamourer.Automation;
using Glamourer.Designs.History;
using Glamourer.Events;
using Glamourer.Services;
using OtterGui.Services;
@ -67,7 +68,7 @@ public sealed class DesignLinkManager : IService, IDisposable
_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)
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.Interop.Material;
using Glamourer.Services;
@ -23,8 +25,7 @@ public class DesignMerger(
modAssociations);
public MergedDesign Merge(IEnumerable<(IDesignStandIn, ApplicationType, JobFlag)> designs, in CustomizeArray currentCustomize,
in DesignData baseRef,
bool respectOwnership, bool modAssociations)
in DesignData baseRef, bool respectOwnership, bool modAssociations)
{
var ret = new MergedDesign(designManager);
ret.Design.SetCustomize(_customize, currentCustomize);
@ -42,19 +43,24 @@ public class DesignMerger(
if (!data.IsHuman)
continue;
var (equipFlags, customizeFlags, crestFlags, parameterFlags, applyMeta) = type.ApplyWhat(design);
ReduceMeta(data, applyMeta, ret, source);
ReduceCustomize(data, customizeFlags, ref fixFlags, ret, source, respectOwnership, startBodyType);
ReduceEquip(data, equipFlags, ret, source, respectOwnership);
ReduceMainhands(data, jobs, equipFlags, ret, source, respectOwnership);
ReduceOffhands(data, jobs, equipFlags, ret, source, respectOwnership);
ReduceCrests(data, crestFlags, ret, source);
ReduceParameters(data, parameterFlags, ret, source);
var collection = type.ApplyWhat(design);
ReduceMeta(data, collection.Meta, ret, source);
ReduceCustomize(data, collection.Customize, ref fixFlags, ret, source, respectOwnership, startBodyType);
ReduceEquip(data, collection.Equip, ret, source, respectOwnership);
ReduceBonusItems(data, collection.BonusItem, ret, source, respectOwnership);
ReduceMainhands(data, jobs, collection.Equip, ret, source, respectOwnership);
ReduceOffhands(data, jobs, collection.Equip, ret, source, respectOwnership);
ReduceCrests(data, collection.Crest, ret, source);
ReduceParameters(data, collection.Parameters, ret, source);
ReduceMods(design as Design, ret, modAssociations);
if (type.HasFlag(ApplicationType.GearCustomization))
ReduceMaterials(design, ret);
if (design.ForcedRedraw)
ret.ForcedRedraw = true;
if (design.ResetAdvancedDyes)
ret.ResetAdvancedDyes = true;
if (design.ResetTemporarySettings)
ret.ResetTemporarySettings = true;
}
ApplyFixFlags(ret, fixFlags);
@ -83,7 +89,7 @@ public class DesignMerger(
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)
return;
@ -100,7 +106,7 @@ public class DesignMerger(
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)
return;
@ -118,7 +124,7 @@ public class DesignMerger(
private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret,
StateSource source)
{
parameterFlags &= ~ret.Design.ApplyParameters;
parameterFlags &= ~ret.Design.Application.Parameters;
if (parameterFlags == 0)
return;
@ -136,7 +142,7 @@ public class DesignMerger(
private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership)
{
equipFlags &= ~ret.Design.ApplyEquip;
equipFlags &= ~ret.Design.Application.Equip;
if (equipFlags == 0)
return;
@ -174,6 +180,22 @@ public class DesignMerger(
}
}
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)
{

View file

@ -14,6 +14,16 @@ public sealed class LinkContainer : List<DesignLink>
public new int Count
=> base.Count + After.Count;
public LinkContainer Clone()
{
var ret = new LinkContainer();
ret.EnsureCapacity(base.Count);
ret.After.EnsureCapacity(After.Count);
ret.AddRange(this);
ret.After.AddRange(After);
return ret;
}
public bool Reorder(int fromIndex, LinkOrder fromOrder, int toIndex, LinkOrder toOrder)
{
var fromList = fromOrder switch
@ -89,13 +99,15 @@ public sealed class LinkContainer : List<DesignLink>
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;
}
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;
}

View file

@ -23,8 +23,8 @@ public readonly struct WeaponList
_list.Add(type, list);
}
var remainingFlags = list.Select(t => t.Item3)
.Aggregate(flags, (current, existingFlags) => current & ~existingFlags);
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;
@ -33,7 +33,7 @@ public readonly struct WeaponList
return true;
}
public bool TryGet(FullEquipType type, JobId id, out (EquipItem, StateSource) ret)
public bool TryGet(FullEquipType type, JobId id, bool gameStateAllowed, out (EquipItem, StateSource) ret)
{
if (!_list.TryGetValue(type, out var list))
{
@ -45,7 +45,7 @@ public readonly struct WeaponList
foreach (var (item, source, flags) in list)
{
if (flags.HasFlag(flag))
if (flags.HasFlag(flag) && (gameStateAllowed || source is not StateSource.Game))
{
ret = (item, source);
return true;
@ -64,12 +64,8 @@ public sealed class MergedDesign
{
public MergedDesign(DesignManager designManager)
{
Design = designManager.CreateTemporary();
Design.ApplyEquip = 0;
Design.ApplyCustomize = 0;
Design.ApplyCrest = 0;
Design.ApplyParameters = 0;
Design.ApplyMeta = 0;
Design = designManager.CreateTemporary();
Design.Application = ApplicationCollection.None;
}
public MergedDesign(DesignBase design)
@ -104,4 +100,6 @@ public sealed class MergedDesign
public readonly SortedList<Mod, ModSettings> AssociatedMods = [];
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;
@ -9,23 +10,15 @@ public enum MetaIndex
VisorState = StateIndex.MetaVisorState,
WeaponState = StateIndex.MetaWeaponState,
ModelId = StateIndex.MetaModelId,
}
[Flags]
public enum MetaFlag : byte
{
Wetness = 0x01,
HatState = 0x02,
VisorState = 0x04,
WeaponState = 0x08,
EarState = StateIndex.MetaEarState,
}
public static class MetaExtensions
{
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)
=> index switch
@ -34,6 +27,7 @@ public static class MetaExtensions
MetaIndex.HatState => MetaFlag.HatState,
MetaIndex.VisorState => MetaFlag.VisorState,
MetaIndex.WeaponState => MetaFlag.WeaponState,
MetaIndex.EarState => MetaFlag.EarState,
_ => (MetaFlag)byte.MaxValue,
};
@ -44,9 +38,24 @@ public static class MetaExtensions
MetaFlag.HatState => MetaIndex.HatState,
MetaFlag.VisorState => MetaIndex.VisorState,
MetaFlag.WeaponState => MetaIndex.WeaponState,
MetaFlag.EarState => MetaIndex.EarState,
_ => (MetaIndex)byte.MaxValue,
};
public static IEnumerable<MetaIndex> ToIndices(this MetaFlag index)
{
if (index.HasFlag(MetaFlag.Wetness))
yield return MetaIndex.Wetness;
if (index.HasFlag(MetaFlag.HatState))
yield return MetaIndex.HatState;
if (index.HasFlag(MetaFlag.VisorState))
yield return MetaIndex.VisorState;
if (index.HasFlag(MetaFlag.WeaponState))
yield return MetaIndex.WeaponState;
if (index.HasFlag(MetaFlag.EarState))
yield return MetaIndex.EarState;
}
public static string ToName(this MetaIndex index)
=> index switch
{
@ -54,6 +63,7 @@ public static class MetaExtensions
MetaIndex.VisorState => "Visor Toggled",
MetaIndex.WeaponState => "Weapon Visible",
MetaIndex.Wetness => "Force Wetness",
MetaIndex.EarState => "Ears Visible",
_ => "Unknown Meta",
};
@ -64,6 +74,7 @@ public static class MetaExtensions
MetaIndex.VisorState => "Toggle the visor state of the characters head gear.",
MetaIndex.WeaponState => "Hide or show the characters weapons when not drawn.",
MetaIndex.Wetness => "Force the character to be wet or not.",
MetaIndex.EarState => "Hide or show the characters ears through the head gear. (Viera only)",
_ => string.Empty,
};
}

View file

@ -39,8 +39,8 @@ public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IServ
public StateSource AssociatedSource()
=> StateSource.Manual;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks
=> combo.Design?.AllLinks ?? [];
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
=> combo.Design?.AllLinks(newApplication) ?? [];
public void AddData(JObject jObj)
{ }
@ -53,4 +53,10 @@ public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IServ
public bool ForcedRedraw
=> combo.Design?.ForcedRedraw ?? false;
public bool ResetAdvancedDyes
=> combo.Design?.ResetAdvancedDyes ?? false;
public bool ResetTemporarySettings
=> combo.Design?.ResetTemporarySettings ?? false;
}

View file

@ -12,7 +12,8 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn
public const string ResolvedName = "Random";
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 _)
=> ResolvedName;
@ -40,45 +41,62 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn
public bool Equals(IDesignStandIn? other)
=> other is RandomDesign r
&& r.ResetOnRedraw == ResetOnRedraw
&& string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates),
StringComparison.OrdinalIgnoreCase);
public StateSource AssociatedSource()
=> StateSource.Manual;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
{
get
{
if (newApplication || ResetOnRedraw)
_currentDesign = rng.Design(Predicates);
if (_currentDesign == null)
yield break;
else
_currentDesign ??= rng.Design(Predicates);
if (_currentDesign == null)
yield break;
foreach (var (link, type, jobs) in _currentDesign.AllLinks)
yield return (link, type, jobs);
}
foreach (var (link, type, jobs) in _currentDesign.AllLinks(newApplication))
yield return (link, type, jobs);
}
public void AddData(JObject jObj)
{
jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates);
jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates);
jObj["ResetOnRedraw"] = ResetOnRedraw;
}
public void ParseData(JObject jObj)
{
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)
{
if (data is not List<IDesignPredicate> predicates)
return false;
if (data is List<IDesignPredicate> predicates)
{
Predicates = predicates;
return true;
}
Predicates = predicates;
return true;
if (data is bool resetOnRedraw)
{
ResetOnRedraw = resetOnRedraw;
return true;
}
return false;
}
public bool ForcedRedraw
=> false;
=> _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;
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)
{
if (localDesigns.Count == 0)
if (localDesigns.Count is 0)
return null;
var idx = _rng.Next(0, localDesigns.Count);
Glamourer.Log.Verbose($"[Random Design] Chose design {idx + 1} out of {localDesigns.Count}: {localDesigns[idx].Incognito}.");
return localDesigns[idx];
if (localDesigns.Count is 1)
{
_lastDesign.SetTarget(localDesigns[idx]);
return localDesigns[idx];
}
if (config.PreventRandomRepeats && _lastDesign.TryGetTarget(out var lastDesign))
while (lastDesign == localDesigns[idx])
idx = _rng.Next(0, localDesigns.Count);
var design = localDesigns[idx];
Glamourer.Log.Verbose($"[Random Design] Chose design {idx + 1} out of {localDesigns.Count}: {design.Incognito}.");
_lastDesign.SetTarget(design);
return design;
}
public Design? Design()
@ -24,12 +38,12 @@ public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileS
public Design? Design(IReadOnlyList<IDesignPredicate> predicates)
{
if (predicates.Count == 0)
return Design();
if (predicates.Count == 1)
return Design(predicates[0]);
return Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList());
return predicates.Count switch
{
0 => Design(),
1 => Design(predicates[0]),
_ => Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()),
};
}
public Design? Design(string restrictions)

View file

@ -22,7 +22,7 @@ public interface IDesignPredicate
: designs;
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

View file

@ -29,9 +29,9 @@ public class RevertDesign : IDesignStandIn
public StateSource AssociatedSource()
=> StateSource.Game;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool _)
{
get { yield return (this, ApplicationType.All, JobFlag.All); }
yield return (this, ApplicationType.All, JobFlag.All);
}
public void AddData(JObject jObj)
@ -45,4 +45,10 @@ public class RevertDesign : IDesignStandIn
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.Services;
using Newtonsoft.Json;
@ -20,6 +20,10 @@ public class EphemeralConfig : ISavable
public Guid SelectedQuickDesign { get; set; } = Guid.Empty;
public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion;
public float CurrentDesignSelectorWidth { get; set; } = 200f;
public float DesignSelectorMinimumScale { get; set; } = 0.1f;
public float DesignSelectorMaximumScale { get; set; } = 0.5f;
[JsonIgnore]
private readonly SaveService _saveService;

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>
ChangedBase,
/// <summary> Change the resetting of temporary settings for a given set. Additional data is the new value. </summary>
ChangedTemporarySettingsReset,
/// <summary> Add a new associated design to a given set. Additional data is the index it got added at [int]. </summary>
AddedDesign,

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.History;
using Glamourer.Gui;
using OtterGui.Classes;
@ -13,107 +14,122 @@ namespace Glamourer.Events;
/// </list>
/// </summary>
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
{
/// <summary> A new design was created. Data is a potential path to move it to [string?]. </summary>
/// <summary> A new design was created. </summary>
Created,
/// <summary> An existing design was deleted. Data is null. </summary>
/// <summary> An existing design was deleted. </summary>
Deleted,
/// <summary> Invoked on full reload. Design and Data are null. </summary>
/// <summary> Invoked on full reload. </summary>
ReloadedAll,
/// <summary> An existing design was renamed. Data is the prior name [string]. </summary>
/// <summary> An existing design was renamed. </summary>
Renamed,
/// <summary> An existing design had its description changed. Data is the prior description [string]. </summary>
/// <summary> An existing design had its description changed. </summary>
ChangedDescription,
/// <summary> An existing design had its associated color changed. Data is the prior color [string]. </summary>
/// <summary> An existing design had its associated color changed. </summary>
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,
/// <summary> An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. </summary>
/// <summary> An existing design had an existing tag removed. </summary>
RemovedTag,
/// <summary> An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. </summary>
/// <summary> An existing design had an existing tag renamed. </summary>
ChangedTag,
/// <summary> An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
/// <summary> An existing design had a new associated mod added. </summary>
AddedMod,
/// <summary> An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
/// <summary> An existing design had an existing associated mod removed. </summary>
RemovedMod,
/// <summary> An existing design had a 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,
/// <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,
/// <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,
/// <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,
/// <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,
/// <summary> An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <summary> An existing design had a stain changed. </summary>
Stains,
/// <summary> An existing design 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,
/// <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,
/// <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,
/// <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,
/// <summary> An existing design had changed whether it always forces a redraw or not. </summary>
ForceRedraw,
/// <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 resets advanced dyes or not. </summary>
ResetAdvancedDyes,
/// <summary> An existing design had changed whether it always resets all prior temporary settings or not. </summary>
ResetTemporarySettings,
/// <summary> An existing design changed whether a specific customization is applied. </summary>
ApplyCustomize,
/// <summary> An existing design changed whether a specific equipment 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,
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
/// <summary> An existing design changed whether a specific bonus item is applied. </summary>
ApplyBonusItem,
/// <summary> An existing design changed whether a specific stain is applied. </summary>
ApplyStain,
/// <summary> An existing design changed 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,
/// <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,
/// <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,
/// <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,
/// <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,
/// <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,
}
public enum Priority
{
/// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChange"/>
/// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChanged"/>
DesignLinkManager = 1,
/// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/>
@ -125,7 +141,10 @@ public sealed class DesignChanged()
/// <seealso cref="Gui.Tabs.DesignTab.DesignFileSystemSelector.OnDesignChange"/>
DesignFileSystemSelector = -1,
/// <seealso cref="SpecialDesignCombo.OnDesignChange"/>
/// <seealso cref="DesignComboBase.OnDesignChanged"/>
DesignCombo = -2,
/// <seealso cref="EditorHistory.OnDesignChanged" />
EditorHistory = -1000,
}
}

View file

@ -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>
/// </list>
/// </summary>
public sealed class SlotUpdating()
: EventWrapperRef34<Model, EquipSlot, CharacterArmor, ulong, SlotUpdating.Priority>(nameof(SlotUpdating))
public sealed class EquipSlotUpdating()
: EventWrapperRef34<Model, EquipSlot, CharacterArmor, ulong, EquipSlotUpdating.Priority>(nameof(EquipSlotUpdating))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnSlotUpdating"/>
/// <seealso cref="State.StateListener.OnEquipSlotUpdating"/>
StateListener = 0,
}
}
}

View file

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

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

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

View file

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

View file

@ -1,7 +1,9 @@
using Glamourer.Api.Enums;
using Glamourer.Designs.History;
using Glamourer.Interop.Structs;
using Glamourer.State;
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
@ -15,11 +17,17 @@ namespace Glamourer.Events;
/// </list>
/// </summary>
public sealed class StateChanged()
: EventWrapper<StateChangeType, StateSource, ActorState, ActorData, object?, StateChanged.Priority>(nameof(StateChanged))
: EventWrapper<StateChangeType, StateSource, ActorState, ActorData, ITransaction?, StateChanged.Priority>(nameof(StateChanged))
{
public enum Priority
{
GlamourerIpc = int.MinValue,
/// <seealso cref="Api.StateApi.OnStateChanged" />
GlamourerIpc = int.MinValue,
/// <seealso cref="Interop.Penumbra.PenumbraAutoRedraw.OnStateChanged" />
PenumbraAutoRedraw = 0,
/// <seealso cref="EditorHistory.OnStateChanged" />
EditorHistory = -1000,
}
}
}

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

@ -19,4 +19,4 @@ public sealed class VisorStateChanged()
/// <seealso cref="State.StateListener.OnVisorChange"/>
StateListener = 0,
}
}
}

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 OtterGui.Classes;
using OtterGui.Services;
@ -32,8 +32,8 @@ public class CustomizeManager : IAsyncDataContainer
}
/// <summary> Get specific icons. </summary>
public IDalamudTextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id)!;
public ISharedImmediateTexture GetIcon(uint id)
=> _icons.TextureProvider.GetFromGameIcon(id);
/// <summary> Iterate over all supported genders and clans. </summary>
public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets()
@ -47,8 +47,8 @@ public class CustomizeManager : IAsyncDataContainer
public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet)
{
_icons = new IconStorage(textures, gameData);
var stopwatch = new Stopwatch();
_icons = new TextureCache(gameData, textures);
var stopwatch = new Stopwatch();
var tmpTask = Task.Run(() =>
{
stopwatch.Start();
@ -72,7 +72,7 @@ public class CustomizeManager : IAsyncDataContainer
public bool Finished
=> Awaiter.IsCompletedSuccessfully;
private readonly IconStorage _icons;
private readonly TextureCache _icons;
private static readonly int ListSize = Clans.Count * Genders.Count;
private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize];

View file

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

View file

@ -16,20 +16,27 @@ public enum CustomizeParameterFlag : ushort
FacePaintUvMultiplier = 0x0400,
FacePaintUvOffset = 0x0800,
DecalColor = 0x1000,
LeftLimbalIntensity = 0x2000,
RightLimbalIntensity = 0x4000,
}
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
& ~(RgbaQuadruples | Percentages | Values);
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 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> RgbFlags = AllFlags.Where(f => RgbTriples.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.LeftEye => "Left Eye Color",
CustomizeParameterFlag.RightEye => "Right Eye Color",
CustomizeParameterFlag.FeatureColor => "Tattoo Color",
CustomizeParameterFlag.FeatureColor => "Feature Color",
CustomizeParameterFlag.FacePaintUvMultiplier => "Multiplier for Face Paint",
CustomizeParameterFlag.FacePaintUvOffset => "Offset of Face Paint",
CustomizeParameterFlag.DecalColor => "Face Paint Color",
CustomizeParameterFlag.LeftLimbalIntensity => "Left Limbal Ring Intensity",
CustomizeParameterFlag.RightLimbalIntensity => "Right Limbal Ring Intensity",
_ => string.Empty,
};
}

View file

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

View file

@ -1,9 +1,9 @@
using Dalamud;
using Dalamud.Game;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using OtterGui.Classes;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
@ -13,11 +13,11 @@ namespace Glamourer.GameData;
internal class CustomizeSetFactory(
IDataManager _gameData,
IPluginLog _log,
IconStorage _icons,
TextureCache _icons,
NpcCustomizeSet _npcCustomizeSet,
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))
{ }
@ -28,10 +28,10 @@ internal class CustomizeSetFactory(
var row = _charaMakeSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var hrothgar = race.ToRace() == Race.Hrothgar;
// Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizeSet(race, gender)
var set = new CustomizeSet(_npcCustomizeSet, race, gender)
{
Name = GetName(race, gender),
Voices = row.Voices,
Voices = row.VoiceStruct,
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
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>
private void SetPostProcessing(CustomizeSet set, CharaMakeParams row)
private void SetPostProcessing(CustomizeSet set, in CharaMakeType row)
{
SetAvailability(set, row);
SetFacialFeatures(set, row);
@ -76,18 +76,16 @@ internal class CustomizeSetFactory(
CustomizeIndex.Hairstyle,
CustomizeIndex.LipColor,
CustomizeIndex.SkinColor,
CustomizeIndex.FacePaintColor,
CustomizeIndex.HighlightsColor,
CustomizeIndex.HairColor,
CustomizeIndex.FacePaint,
CustomizeIndex.TattooColor,
CustomizeIndex.EyeColorLeft,
CustomizeIndex.EyeColorRight,
CustomizeIndex.TailShape,
};
var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>();
var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>()
{
(CustomizeIndex.Height, CustomizeValue.Max),
};
_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)
{
@ -106,10 +104,10 @@ internal class CustomizeSetFactory(
}
private readonly ColorParameters _colorParameters = new(_gameData, _log);
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet = _gameData.GetExcelSheet<CharaMakeCustomize>(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<Tribe> _tribeSheet = _gameData.GetExcelSheet<Tribe>(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<RawRow> _hairSheet = _gameData.GetExcelSheet<RawRow>(ClientLanguage.English, "HairMakeType");
private readonly ExcelSheet<Tribe> _tribeSheet = _gameData.GetExcelSheet<Tribe>(ClientLanguage.English);
// Those color pickers are shared between all races.
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[] _tattooColorPicker = CreateColors(_colors, CustomizeIndex.TattooColor, 0, 192);
private readonly ExcelSheet<CharaMakeParams> _charaMakeSheet = _gameData.Excel
.GetType()
.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
.MakeGenericMethod(typeof(CharaMakeParams))
.Invoke(_gameData.Excel, ["charamaketype", _gameData.Language.ToLumina(), null])! as ExcelSheet<CharaMakeParams>
?? null!;
private readonly ExcelSheet<CharaMakeType> _charaMakeSheet = _gameData.Excel.GetSheet<CharaMakeType>();
/// <summary> Obtain available skin and hair colors for the given clan and gender. </summary>
private (CustomizeData[] Skin, CustomizeData[] Hair) GetSkinHairColors(SubRace race, Gender gender)
@ -144,29 +137,28 @@ internal class CustomizeSetFactory(
private string GetName(SubRace race, Gender gender)
=> gender switch
{
Gender.Male => _tribeSheet.GetRow((uint)race)?.Masculine.ToDalamudString().TextValue ?? race.ToName(),
Gender.Female => _tribeSheet.GetRow((uint)race)?.Feminine.ToDalamudString().TextValue ?? race.ToName(),
Gender.Male => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Masculine.ExtractText() : race.ToName(),
Gender.Female => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Feminine.ExtractText() : race.ToName(),
_ => "Unknown",
};
/// <summary> Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. </summary>
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.
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.
for (var i = 0; i < row.Unknown30; ++i)
for (var i = 0; i < numHairs; ++i)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
// Hairs start at Unknown66.
var customizeIdx = row.ReadUInt32Column(66 + i * 9);
if (customizeIdx == uint.MaxValue)
continue;
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx);
if (hairRow == null)
if (!_customizeSheet.TryGetRow(customizeIdx, out var hairRow))
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx));
else if (_icons.IconExists(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>
private CustomizeData[] GetTailEarShapes(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray()
?? [];
private CustomizeData[] GetTailEarShapes(CharaMakeType row)
=> ExtractValues(row, CustomizeIndex.TailShape);
/// <summary> Specific icons for faces. </summary>
private CustomizeData[] GetFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx)
?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray()
?? [];
private CustomizeData[] GetFaces(CharaMakeType row)
=> ExtractValues(row, CustomizeIndex.Face);
/// <summary> Specific icons for Hrothgar patterns. </summary>
private CustomizeData[] HrothgarFurPattern(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray()
?? [];
private CustomizeData[] HrothgarFurPattern(CharaMakeType row)
=> ExtractValues(row, CustomizeIndex.LipColor);
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>
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender);
// 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.
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
var customizeIdx = row.ReadUInt32Column(73 + i * 9);
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
// 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,
(ushort)paintRow.RowId));
else
@ -226,21 +213,18 @@ internal class CustomizeSetFactory(
}
/// <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 menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == gameId);
return menu?.Size ?? 0;
var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId);
return menu?.SubMenuNum ?? 0;
}
/// <summary> Get generic Features. </summary>
private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new CustomizeData(id, (CustomizeValue)(index + 1), value)
: new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId);
}
=> _customizeSheet.TryGetRow(value, out var row)
? new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId)
: new CustomizeData(id, (CustomizeValue)(index + 1), value);
/// <summary> Create generic color sets from the parameters. </summary>
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>
private string[] GetOptionNames(CharaMakeParams row)
private string[] GetOptionNames(CharaMakeType row)
{
var nameArray = Enum.GetValues<CustomizeIndex>().Select(c =>
{
// Find the first menu that corresponds to the Id.
var byteId = c.ToByteAndMask().ByteIdx;
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == byteId);
var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == byteId);
if (menu == null)
{
// If none exists and the id corresponds to highlights, set the Highlights name.
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.
return c.ToDefaultName();
}
// 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(textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName());
return string.Intern(_lobbySheet.TryGetRow(menu.Value.Menu.RowId, out var textRow)
? textRow.Text.ExtractText()
: c.ToDefaultName());
}).ToArray();
// Add names for both eye colors.
@ -300,7 +283,7 @@ internal class CustomizeSetFactory(
}
/// <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.
return Enum.GetValues<CustomizeIndex>().Select(c =>
@ -312,13 +295,13 @@ internal class CustomizeSetFactory(
case CustomizeIndex.EyeColorLeft:
case CustomizeIndex.EyeColorRight:
case CustomizeIndex.FacePaintColor:
return CharaMakeParams.MenuType.ColorPicker;
case CustomizeIndex.BodyType: return CharaMakeParams.MenuType.Nothing;
return MenuType.ColorPicker;
case CustomizeIndex.BodyType: return MenuType.Nothing;
case CustomizeIndex.FacePaintReversed:
case CustomizeIndex.Highlights:
case CustomizeIndex.SmallIris:
case CustomizeIndex.Lipstick:
return CharaMakeParams.MenuType.Checkmark;
return MenuType.Checkmark;
case CustomizeIndex.FacialFeature1:
case CustomizeIndex.FacialFeature2:
case CustomizeIndex.FacialFeature3:
@ -327,29 +310,23 @@ internal class CustomizeSetFactory(
case CustomizeIndex.FacialFeature6:
case CustomizeIndex.FacialFeature7:
case CustomizeIndex.LegacyTattoo:
return CharaMakeParams.MenuType.IconCheckmark;
return MenuType.IconCheckmark;
}
var gameId = c.ToByteAndMask().ByteIdx;
// Otherwise find the first menu corresponding to the id.
// If there is none, assume a list.
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == gameId);
var ret = menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
if (c is CustomizeIndex.TailShape && ret is CharaMakeParams.MenuType.ListSelector)
ret = CharaMakeParams.MenuType.List1Selector;
var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId);
var ret = (MenuType)(menu?.SubMenuType ?? (byte)MenuType.ListSelector);
if (c is CustomizeIndex.TailShape && ret is MenuType.ListSelector)
ret = MenuType.List1Selector;
return ret;
}).ToArray();
}
/// <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(set.Faces.Count > 0, CustomizeIndex.Face);
Set(true, CustomizeIndex.Hairstyle);
@ -399,7 +376,7 @@ internal class CustomizeSetFactory(
ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor;
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, []);
set.Order = dict;
}
@ -423,7 +400,7 @@ internal class CustomizeSetFactory(
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;
}
}
@ -435,7 +412,7 @@ internal class CustomizeSetFactory(
/// 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.
/// </summary>
private static void SetFacialFeatures(CustomizeSet set, CharaMakeParams row)
private static void SetFacialFeatures(CustomizeSet set, in CharaMakeType row)
{
var count = set.Faces.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();
for (var i = 0; i < count; ++i)
{
var data = row.FacialFeatureByFace[i].Icons;
tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]);
tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]);
tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]);
tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]);
tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]);
tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]);
tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]);
var data = row.FacialFeatureOption[i];
tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, (uint)data.Option1);
tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, (uint)data.Option2);
tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, (uint)data.Option3);
tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, (uint)data.Option4);
tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, (uint)data.Option5);
tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, (uint)data.Option6);
tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, (uint)data.Option7);
}
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.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using OtterGui.Services;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
@ -35,32 +37,51 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
/// <summary> The list of data. </summary>
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>
public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{
var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter);
Awaiter = waitTask.ContinueWith(_ =>
{
var watch = Stopwatch.StartNew();
var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs));
var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames));
FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result);
Time = watch.ElapsedMilliseconds;
});
{
var watch = Stopwatch.StartNew();
var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs));
var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames));
FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result);
Time = watch.ElapsedMilliseconds;
})
.ContinueWith(_ => CheckFacepaintFiles(data, _facepaints));
}
/// <summary> Create data from event NPCs. </summary>
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);
// Go through all event NPCs already collected into a dictionary.
foreach (var (id, name) in eNpcs)
{
var row = enpcSheet.GetRow(id.Id);
// We only accept NPCs with valid names.
if (row == null || name.IsNullOrWhitespace())
if (!enpcSheet.TryGetRow(id.Id, out var row) || name.IsNullOrWhitespace())
continue;
// Check if the customization is a valid human.
@ -72,33 +93,17 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
{
Name = name,
Customize = customize,
ModelId = row.ModelChara.Row,
ModelId = row.ModelChara.RowId,
Id = id,
Kind = ObjectKind.EventNpc,
};
// 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.
if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip)
{
// Prefer the NpcEquip reference if it is set and the own does not appear to be set, otherwise use the own.
if (row.NpcEquip.RowId != 0 && row.NpcEquip.Value is { } equip && row is { ModelBody: 0, ModelLegs: 0 })
ApplyNpcEquip(ref ret, equip);
}
else
{
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;
}
ApplyNpcEquip(ref ret, row);
list.Add(ret);
}
@ -109,14 +114,14 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
/// <summary> Create data from battle NPCs. </summary>
private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{
var bnpcSheet = data.GetExcelSheet<BNpcBase>()!;
var list = new List<NpcData>((int)bnpcSheet.RowCount);
var bnpcSheet = data.GetExcelSheet<BNpcBase>();
var list = new List<NpcData>(bnpcSheet.Count);
// We go through all battle NPCs in the sheet because the dictionary refers to names.
foreach (var baseRow in bnpcSheet)
{
// Only accept humans.
if (baseRow.ModelChara.Value!.Type != 1)
if (baseRow.ModelChara.Value.Type != 1)
continue;
var bnpcNameIds = bNpcNames[baseRow.RowId];
@ -125,15 +130,15 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
continue;
// Check if the customization is a valid human.
var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!);
var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value);
if (!valid)
continue;
var equip = baseRow.NpcEquip.Value!;
var equip = baseRow.NpcEquip.Value;
var ret = new NpcData
{
Customize = customize,
ModelId = baseRow.ModelChara.Row,
ModelId = baseRow.ModelChara.RowId,
Id = baseRow.RowId,
Kind = ObjectKind.BattleNpc,
};
@ -164,6 +169,12 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
for (var i = 0; i < duplicates.Count; ++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)
{
if (current.DataEquals(duplicates[j]))
@ -202,18 +213,36 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
/// <summary> Apply equipment from a NpcEquip row. </summary>
private static void ApplyNpcEquip(ref NpcData data, NpcEquip row)
{
data.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
data.Set(1, row.ModelBody | (row.DyeBody.Row << 24));
data.Set(2, row.ModelHands | (row.DyeHands.Row << 24));
data.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24));
data.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24));
data.Set(5, row.ModelEars | (row.DyeEars.Row << 24));
data.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24));
data.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24));
data.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24));
data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24));
data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48));
data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48));
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;
}
/// <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;
}
@ -221,11 +250,11 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize)
{
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(2, (CustomizeValue)bnpcCustomize.BodyType);
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(6, (CustomizeValue)bnpcCustomize.HairStyle);
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>
private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase)
{
if (enpcBase.ModelChara.Value?.Type != 1)
if (enpcBase.ModelChara.ValueNullable?.Type != 1)
return (false, CustomizeArray.Default);
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(2, (CustomizeValue)enpcBase.BodyType);
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(6, (CustomizeValue)enpcBase.HairStyle);
customize.SetByIndex(7, (CustomizeValue)enpcBase.HairHighlight);
@ -298,6 +327,17 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
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/>
public IEnumerator<NpcData> GetEnumerator()
=> _data.GetEnumerator();

View file

@ -13,7 +13,7 @@ public unsafe struct NpcData
public CustomizeArray Customize;
/// <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>
public CharacterWeapon Mainhand;
@ -54,36 +54,35 @@ public unsafe struct NpcData
{
sb.Append(span[i].Set.Id.ToString("D4"))
.Append('-')
.Append(span[i].Variant.Id.ToString("D3"))
.Append('-')
.Append(span[i].Stain.Id.ToString("D3"))
.Append(", ");
.Append(span[i].Variant.Id.ToString("D3"));
foreach (var stain in span[i].Stains)
sb.Append('-').Append(stain.Id.ToString("D3"));
}
sb.Append(Mainhand.Skeleton.Id.ToString("D4"))
.Append('-')
.Append(Mainhand.Weapon.Id.ToString("D4"))
.Append('-')
.Append(Mainhand.Variant.Id.ToString("D3"))
.Append('-')
.Append(Mainhand.Stain.Id.ToString("D4"))
.Append(", ")
.Append(Mainhand.Variant.Id.ToString("D3"));
foreach (var stain in Mainhand.Stains)
sb.Append('-').Append(stain.Id.ToString("D3"));
sb.Append(", ")
.Append(Offhand.Skeleton.Id.ToString("D4"))
.Append('-')
.Append(Offhand.Weapon.Id.ToString("D4"))
.Append('-')
.Append(Offhand.Variant.Id.ToString("D3"))
.Append('-')
.Append(Offhand.Stain.Id.ToString("D3"));
.Append(Offhand.Variant.Id.ToString("D3"));
foreach (var stain in Mainhand.Stains)
sb.Append('-').Append(stain.Id.ToString("D3"));
return sb.ToString();
}
/// <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)
{
((uint*)ptr)[idx] = value;
((ulong*)ptr)[idx] = value;
}
}

View file

@ -1,5 +1,7 @@
using Dalamud.Plugin;
using Glamourer.Api;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.Services;
@ -7,6 +9,7 @@ using Glamourer.State;
using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.GameData.Interop;
namespace Glamourer;
@ -23,15 +26,17 @@ public class Glamourer : IDalamudPlugin
public static readonly Logger Log = new();
public static MessageService Messager { get; private set; } = null!;
public static DynamisIpc Dynamis { get; private set; } = null!;
private readonly ServiceManager _services;
public Glamourer(DalamudPluginInterface pluginInterface)
public Glamourer(IDalamudPluginInterface pluginInterface)
{
try
{
_services = StaticServiceManager.CreateProvider(pluginInterface, Log);
_services = StaticServiceManager.CreateProvider(pluginInterface, Log, this);
Messager = _services.GetService<MessageService>();
Dynamis = _services.GetService<DynamisIpc>();
_services.EnsureRequiredServices();
_services.GetService<VisorService>();
@ -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()
=> _services?.Dispose();

View file

@ -1,92 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer</AssemblyName>
<FileVersion>9.0.0.1</FileVersion>
<AssemblyVersion>9.0.0.1</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2023</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<Copyright>Copyright © 2025</Copyright>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<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>
<ItemGroup>
<None Remove="LegacyTattoo.raw" />
<None Include="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="LegacyTattoo.raw" />
</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>
<ProjectReference Include="..\Glamourer.Api\Glamourer.Api.csproj" />
<ProjectReference Include="..\OtterGui\OtterGui.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" />
<PackageReference Include="Vortice.Direct3D11" Version="3.4.2-beta" />
</ItemGroup>
@ -116,14 +56,4 @@
<InformationalVersion>$(GitCommitHash)</InformationalVersion>
</PropertyGroup>
</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>

View file

@ -8,7 +8,7 @@
"AssemblyVersion": "9.0.0.1",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 9,
"DalamudApiLevel": 14,
"ImageUrls": null,
"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;
@ -29,6 +29,10 @@ public enum ColorId
TriStateNeutral,
BattleNpc,
EventNpc,
ModdedItemMarker,
ContainsItemsEnabled,
ContainsItemsDisabled,
AdvancedDyeActive,
}
public static class Colors
@ -39,32 +43,36 @@ public static class Colors
=> color switch
{
// @formatter:off
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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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
};

View file

@ -1,16 +1,83 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.GameData;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using System;
namespace Glamourer.Gui.Customization;
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)
{
@ -21,13 +88,18 @@ public partial class CustomizationDrawer
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);
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);
UpdateValue(data.Value);
}
DrawDragDropSource(index, custom);
DrawDragDropTarget(index);
}
var npc = false;
@ -70,7 +142,7 @@ public partial class CustomizationDrawer
for (var i = 0; i < _currentCount; ++i)
{
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);
ImGui.CloseCurrentPopup();

View file

@ -1,5 +1,5 @@
using Dalamud.Interface;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
@ -34,14 +34,13 @@ public partial class CustomizationDrawer
private void DrawGenderSelector()
{
using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw))
using (ImRaii.Disabled(_locked || _lockedRedraw))
{
var icon = _customize.Gender switch
{
Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble,
Gender.Male => FontAwesomeIcon.Mars,
Gender.Female => FontAwesomeIcon.Venus,
_ => FontAwesomeIcon.Question,
Gender.Male => FontAwesomeIcon.Mars,
Gender.Female => FontAwesomeIcon.Venus,
_ => FontAwesomeIcon.Question,
};
if (ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty,
@ -56,7 +55,7 @@ public partial class CustomizationDrawer
private void DrawRaceCombo()
{
using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw))
using (ImRaii.Disabled(_locked || _lockedRedraw))
{
ImGui.SetNextItemWidth(_raceSelectorWidth);
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 ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -29,10 +31,11 @@ public partial class CustomizationDrawer
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))
{
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize))
{
ImGui.OpenPopup(IconSelectorPopup);
}
@ -43,7 +46,8 @@ public partial class CustomizationDrawer
}
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
if (hasIcon)
ImGuiUtil.HoverIconTooltip(wrap!, _iconSize);
ImGui.SameLine();
using (_ = ImRaii.Group())
@ -83,8 +87,9 @@ public partial class CustomizationDrawer
using var frameColor = current == i
? ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed)
: 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);
ImGui.CloseCurrentPopup();
@ -96,8 +101,9 @@ public partial class CustomizationDrawer
else
_favorites.TryAdd(_set.Gender, _set.Clan, _currentIndex, custom.Value);
ImGuiUtil.HoverIconTooltip(icon, _iconSize,
FavoriteManager.TypeAllowed(_currentIndex) ? "Right-Click to toggle favorite." : string.Empty);
if (hasIcon)
ImGuiUtil.HoverIconTooltip(wrap!, _iconSize,
FavoriteManager.TypeAllowed(_currentIndex) ? "Right-Click to toggle favorite." : string.Empty);
var text = custom.Value.ToString();
var textWidth = ImGui.CalcTextSize(text).X;
@ -188,25 +194,36 @@ public partial class CustomizationDrawer
private void DrawMultiIcons()
{
var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark];
var options = _set.Order[MenuType.IconCheckmark];
using var group = ImRaii.Group();
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())
{
using var id = SetId(featureIdx);
var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero;
var feature = _set.Data(featureIdx, 0, face);
var icon = featureIdx == CustomizeIndex.LegacyTattoo
? _legacyTattoo ?? _service.Manager.GetIcon(feature.IconId)
: _service.Manager.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X,
Vector4.Zero, enabled ? Vector4.One : _redTint))
using var id = SetId(featureIdx);
var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero;
var feature = _set.Data(featureIdx, 0, face);
bool hasIcon;
IDalamudTextureWrap? wrap;
var icon = _service.Manager.GetIcon(feature.IconId);
if (featureIdx is CustomizeIndex.LegacyTattoo)
{
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);
Changed |= _currentFlag;
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
if (hasIcon)
ImGuiUtil.HoverIconTooltip(wrap!, _iconSize);
if (idx % 4 != 3)
ImGui.SameLine();
}

View file

@ -1,4 +1,4 @@
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using OtterGuiInternal;
@ -43,11 +43,13 @@ public partial class CustomizationDrawer
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}'')",
_ => FormattableString.Invariant($"({height})"),
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);
}
@ -293,7 +295,7 @@ public partial class CustomizationDrawer
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))
ToggleApply();
}

View file

@ -1,10 +1,10 @@
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services;
using Glamourer.GameData;
using Glamourer.Services;
using Glamourer.Unlocks;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
@ -13,16 +13,15 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer(
DalamudPluginInterface pi,
ITextureProvider textures,
CustomizeService _service,
CodeService _codes,
Configuration _config,
FavoriteManager _favorites,
HeightService _heightService)
: IDisposable
{
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;
@ -117,30 +116,27 @@ public partial class CustomizationDrawer(
try
{
if (_codes.Enabled(CodeService.CodeFlag.Artisan))
return DrawArtisan();
DrawRaceGenderSelector();
DrawBodyType();
_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);
Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.IconSelector], DrawIconSelector, ImGui.SameLine);
Functions.IteratePairwise(_set.Order[MenuType.IconSelector], DrawIconSelector, ImGui.SameLine);
DrawMultiIconSelector();
foreach (var id in _set.Order[CharaMakeParams.MenuType.ListSelector])
foreach (var id in _set.Order[MenuType.ListSelector])
DrawListSelector(id, false);
foreach (var id in _set.Order[CharaMakeParams.MenuType.List1Selector])
foreach (var id in _set.Order[MenuType.List1Selector])
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));
return Changed != 0 || ChangeApply != _initialApply;
}
@ -154,31 +150,6 @@ public partial class CustomizationDrawer(
}
}
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()
{
_spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X };
@ -190,7 +161,7 @@ public partial class CustomizationDrawer(
_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");
if (resource == null)
@ -199,7 +170,7 @@ public partial class CustomizationDrawer(
var rawImage = new byte[resource.Length];
var length = resource.Read(rawImage, 0, (int)resource.Length);
return length == resource.Length
? pi.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4)
? textures.CreateFromRaw(RawImageSpecification.Rgba32(192, 192), rawImage, "Glamourer.LegacyTattoo")
: null;
}
}

View file

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

View file

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

View file

@ -2,11 +2,13 @@
using Dalamud.Interface.Utility.Raii;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Designs.Special;
using Glamourer.Events;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Widgets;
@ -20,6 +22,7 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
protected readonly TabSelected TabSelected;
protected float InnerWidth;
private IDesignStandIn? _currentDesign;
private bool _isCurrentSelectionDirty;
protected DesignComboBase(Func<IReadOnlyList<Tuple<IDesignStandIn, string>>> generator, Logger log, DesignChanged designChanged,
TabSelected tabSelected, EphemeralConfig config, DesignColors designColors)
@ -29,7 +32,7 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
TabSelected = tabSelected;
Config = config;
DesignColors = designColors;
DesignChanged.Subscribe(OnDesignChange, DesignChanged.Priority.DesignCombo);
DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo);
}
public bool Incognito
@ -37,7 +40,7 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
void IDisposable.Dispose()
{
DesignChanged.Unsubscribe(OnDesignChange);
DesignChanged.Unsubscribe(OnDesignChanged);
GC.SuppressFinalize(this);
}
@ -82,17 +85,11 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
DrawRightAligned(quickDesign.ResolveName(false), "[Nothing]", DesignColors.MissingColor);
}
protected override int UpdateCurrentSelected(int currentSelected)
{
CurrentSelectionIdx = Items.IndexOf(p => _currentDesign == p.Item1);
UpdateSelection(CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : null);
return CurrentSelectionIdx;
}
protected bool Draw(IDesignStandIn? currentDesign, string? label, float width)
{
_currentDesign = currentDesign;
InnerWidth = 400 * ImGuiHelpers.GlobalScale;
UpdateCurrentSelection();
InnerWidth = 400 * ImGuiHelpers.GlobalScale;
var name = label ?? "Select Design Here...";
bool ret;
using (_ = currentDesign != null ? ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(currentDesign as Design)) : null)
@ -126,37 +123,60 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
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)
{
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)
{
UpdateSelection(Items[CurrentSelectionIdx]);
}
else if (Items.Count > 0)
{
CurrentSelectionIdx = 0;
UpdateSelection(Items[0]);
}
else
{
UpdateSelection(null);
}
if (!ReferenceEquals(_currentDesign, CurrentSelection?.Item1))
CurrentSelectionIdx = -1;
if (!priorState)
Cleanup();
break;
base.OnMouseWheel(preview, ref _2, steps);
}
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)
@ -174,7 +194,7 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
ImGui.TextUnformatted("Currently resolving to ");
using var color = ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(linkedDesign));
ImGui.SameLine(0, 0);
ImGui.TextUnformatted(linkedDesign.Name);
ImGui.TextUnformatted(linkedDesign.Name.Text);
}
else
{
@ -224,8 +244,7 @@ public abstract class DesignCombo : DesignComboBase
public sealed class QuickDesignCombo : DesignCombo
{
public QuickDesignCombo(DesignManager designs,
DesignFileSystem fileSystem,
public QuickDesignCombo(DesignFileSystem fileSystem,
Logger log,
DesignChanged designChanged,
TabSelected tabSelected,
@ -233,9 +252,9 @@ public sealed class QuickDesignCombo : DesignCombo
DesignColors designColors)
: base(log, designChanged, tabSelected, config, designColors, () =>
[
.. designs.Designs
.Where(d => d.QuickDesign)
.Select(d => new Tuple<IDesignStandIn, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
.. fileSystem
.Where(kvp => kvp.Key.QuickDesign)
.Select(kvp => new Tuple<IDesignStandIn, string>(kvp.Key, kvp.Value.FullName()))
.OrderBy(d => d.Item2),
])
{
@ -276,7 +295,6 @@ public sealed class QuickDesignCombo : DesignCombo
}
public sealed class LinkDesignCombo(
DesignManager designs,
DesignFileSystem fileSystem,
Logger log,
DesignChanged designChanged,
@ -285,8 +303,8 @@ public sealed class LinkDesignCombo(
DesignColors designColors)
: DesignCombo(log, designChanged, tabSelected, config, designColors, () =>
[
.. designs.Designs
.Select(d => new Tuple<IDesignStandIn, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
.. fileSystem
.Select(kvp => new Tuple<IDesignStandIn, string>(kvp.Key, kvp.Value.FullName()))
.OrderBy(d => d.Item2),
]);
@ -300,8 +318,8 @@ public sealed class RandomDesignCombo(
DesignColors designColors)
: DesignCombo(log, designChanged, tabSelected, config, designColors, () =>
[
.. designs.Designs
.Select(d => new Tuple<IDesignStandIn, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
.. fileSystem
.Select(kvp => new Tuple<IDesignStandIn, string>(kvp.Key, kvp.Value.FullName()))
.OrderBy(d => d.Item2),
])
{
@ -327,7 +345,6 @@ public sealed class RandomDesignCombo(
}
public sealed class SpecialDesignCombo(
DesignManager designs,
DesignFileSystem fileSystem,
TabSelected tabSelected,
DesignColors designColors,
@ -337,8 +354,8 @@ public sealed class SpecialDesignCombo(
EphemeralConfig config,
RandomDesignGenerator rng,
QuickSelectedDesign quickSelectedDesign)
: DesignComboBase(() => designs.Designs
.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)
.Prepend(new Tuple<IDesignStandIn, string>(new RandomDesign(rng), string.Empty))
.Prepend(new Tuple<IDesignStandIn, string>(quickSelectedDesign, string.Empty))

View file

@ -6,26 +6,28 @@ using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using Dalamud.Bindings.ImGui;
using OtterGui.Classes;
using OtterGui.Text;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
namespace Glamourer.Gui;
[Flags]
public enum QdbButtons
{
ApplyDesign = 0x01,
RevertAll = 0x02,
RevertAutomation = 0x04,
RevertAdvanced = 0x08,
RevertEquip = 0x10,
RevertCustomize = 0x20,
ReapplyAutomation = 0x40,
ApplyDesign = 0x01,
RevertAll = 0x02,
RevertAutomation = 0x04,
RevertAdvancedDyes = 0x08,
RevertEquip = 0x10,
RevertCustomize = 0x20,
ReapplyAutomation = 0x40,
ResetSettings = 0x80,
RevertAdvancedCustomization = 0x100,
}
public sealed class DesignQuickBar : Window, IDisposable
@ -35,19 +37,21 @@ public sealed class DesignQuickBar : Window, IDisposable
? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove
: ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing;
private readonly Configuration _config;
private readonly QuickDesignCombo _designCombo;
private readonly StateManager _stateManager;
private readonly AutoDesignApplier _autoDesignApplier;
private readonly ObjectManager _objects;
private readonly IKeyState _keyState;
private readonly ImRaii.Style _windowPadding = new();
private readonly ImRaii.Color _windowColor = new();
private DateTime _keyboardToggle = DateTime.UnixEpoch;
private int _numButtons;
private readonly Configuration _config;
private readonly QuickDesignCombo _designCombo;
private readonly StateManager _stateManager;
private readonly AutoDesignApplier _autoDesignApplier;
private readonly ActorObjectManager _objects;
private readonly PenumbraService _penumbra;
private readonly IKeyState _keyState;
private readonly ImRaii.Style _windowPadding = new();
private readonly ImRaii.Color _windowColor = new();
private DateTime _keyboardToggle = DateTime.UnixEpoch;
private int _numButtons;
private readonly StringBuilder _tooltipBuilder = new(512);
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)
{
_config = config;
@ -56,9 +60,11 @@ public sealed class DesignQuickBar : Window, IDisposable
_keyState = keyState;
_objects = objects;
_autoDesignApplier = autoDesignApplier;
_penumbra = penumbra;
IsOpen = _config.Ephemeral.ShowDesignQuickBar;
DisableWindowSounds = true;
Size = Vector2.Zero;
RespectCloseHotkey = false;
}
public void Dispose()
@ -70,6 +76,9 @@ public sealed class DesignQuickBar : Window, IDisposable
IsOpen = _config.Ephemeral.ShowDesignQuickBar && _config.QdbButtons != 0;
}
public override bool DrawConditions()
=> _objects.Player.Valid;
public override void PreDraw()
{
Flags = GetFlags;
@ -100,7 +109,7 @@ public sealed class DesignQuickBar : Window, IDisposable
private void Draw(float width)
{
using var group = ImRaii.Group();
using var group = ImUtf8.Group();
var spacing = ImGui.GetStyle().ItemInnerSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
var buttonSize = new Vector2(ImGui.GetFrameHeight());
@ -112,13 +121,15 @@ public sealed class DesignQuickBar : Window, IDisposable
ImGui.SameLine();
DrawApplyButton(buttonSize);
}
DrawRevertButton(buttonSize);
DrawRevertEquipButton(buttonSize);
DrawRevertCustomizeButton(buttonSize);
DrawRevertAdvancedCustomization(buttonSize);
DrawRevertAdvancedDyes(buttonSize);
DrawRevertAutomationButton(buttonSize);
DrawReapplyAutomationButton(buttonSize);
DrawResetSettingsButton(buttonSize);
}
private ActorIdentifier _playerIdentifier;
@ -141,33 +152,38 @@ public sealed class DesignQuickBar : Window, IDisposable
{
var design = _designCombo.Design as Design;
var available = 0;
var tooltip = string.Empty;
_tooltipBuilder.Clear();
if (design == null)
{
tooltip = "No design selected.";
_tooltipBuilder.Append("No design selected.");
}
else
{
if (_playerIdentifier.IsValid && _playerData.Valid)
{
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 (available != 0)
tooltip += '\n';
_tooltipBuilder.Append('\n');
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)
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();
if (!clicked)
return;
@ -179,9 +195,8 @@ public sealed class DesignQuickBar : Window, IDisposable
return;
}
var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters);
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks);
using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys());
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true });
}
private void DrawRevertButton(Vector2 buttonSize)
@ -190,28 +205,32 @@ public sealed class DesignQuickBar : Window, IDisposable
return;
var available = 0;
var tooltip = string.Empty;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false })
{
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 (available != 0)
tooltip += '\n';
_tooltipBuilder.Append('\n');
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)
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();
if (clicked)
_stateManager.ResetState(state!, StateSource.Manual);
_stateManager.ResetState(state!, StateSource.Manual, isFinal: true);
}
private void DrawRevertAutomationButton(Vector2 buttonSize)
@ -223,34 +242,37 @@ public sealed class DesignQuickBar : Window, IDisposable
return;
var available = 0;
var tooltip = string.Empty;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
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 (available != 0)
tooltip += '\n';
_tooltipBuilder.Append('\n');
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)
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();
if (!clicked)
return;
foreach (var actor in data.Objects)
{
_autoDesignApplier.ReapplyAutomation(actor, id, state!, true, out var forcedRedraw);
_stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual);
_autoDesignApplier.ReapplyAutomation(actor, id, state!, true, false, out var forcedRedraw);
_stateManager.ReapplyAutomationState(actor, forcedRedraw, true, StateSource.Manual);
}
}
@ -263,69 +285,104 @@ public sealed class DesignQuickBar : Window, IDisposable
return;
var available = 0;
var tooltip = string.Empty;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
available |= 1;
tooltip = "Left-Click: Reapply the player character's current automation on top of their current 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 (available != 0)
tooltip += '\n';
_tooltipBuilder.Append('\n');
available |= 2;
tooltip += $"Right-Click: Reapply {_targetIdentifier}'s current automation on top of their current state.";
_tooltipBuilder.Append("Right-Click: Reapply ")
.Append(_targetIdentifier)
.Append("'s current automation on top of their current state.");
}
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.Repeat, 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, out var forcedRedraw);
_stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual);
_autoDesignApplier.ReapplyAutomation(actor, id, state!, false, false, out var forcedRedraw);
_stateManager.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Manual);
}
}
private void DrawRevertAdvancedCustomization(Vector2 buttonSize)
{
if (!_config.UseAdvancedParameters)
return;
if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvanced))
if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization))
return;
var available = 0;
var tooltip = string.Empty;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
available |= 1;
tooltip = "Left-Click: Revert the advanced customizations and dyes of the player character to their game state.";
_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)
tooltip += '\n';
_tooltipBuilder.Append('\n');
available |= 2;
tooltip += $"Right-Click: Revert the advanced customizations and dyes of {_targetIdentifier} to their game state.";
_tooltipBuilder.Append("Right-Click: Revert the advanced customizations of ")
.Append(_targetIdentifier)
.Append(" to their game state.");
}
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.Palette, buttonSize, tooltip, available);
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.PaintBrush, buttonSize, available);
ImGui.SameLine();
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)
@ -334,26 +391,28 @@ public sealed class DesignQuickBar : Window, IDisposable
return;
var available = 0;
var tooltip = string.Empty;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
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 (available != 0)
tooltip += '\n';
_tooltipBuilder.Append('\n');
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)
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();
if (clicked)
_stateManager.ResetCustomize(state!, StateSource.Manual);
@ -365,35 +424,80 @@ public sealed class DesignQuickBar : Window, IDisposable
return;
var available = 0;
var tooltip = string.Empty;
_tooltipBuilder.Clear();
if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid)
{
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 (available != 0)
tooltip += '\n';
_tooltipBuilder.Append('\n');
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)
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();
if (clicked)
_stateManager.ResetEquip(state!, StateSource.Manual);
}
private (bool, ActorIdentifier, ActorData, ActorState?) ResolveTarget(FontAwesomeIcon icon, Vector2 buttonSize, string tooltip,
int available)
private void DrawResetSettingsButton(Vector2 buttonSize)
{
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))
return (true, _playerIdentifier, _playerData, _playerState);
if ((available & 2) == 2 && ImGui.IsItemClicked(ImGuiMouseButton.Right))
@ -433,12 +537,16 @@ public sealed class DesignQuickBar : Window, IDisposable
++_numButtons;
}
if ((_config.UseAdvancedParameters || _config.UseAdvancedDyes) && _config.QdbButtons.HasFlag(QdbButtons.RevertAdvanced))
if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization))
++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes))
++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.RevertCustomize))
++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.RevertEquip))
++_numButtons;
if (_config.UseTemporarySettings && _config.QdbButtons.HasFlag(QdbButtons.ResetSettings))
++_numButtons;
if (_config.QdbButtons.HasFlag(QdbButtons.ApplyDesign))
{
++_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.Interop.Material;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -7,9 +8,9 @@ namespace Glamourer.Gui.Equipment;
public struct EquipDrawData(EquipSlot slot, in DesignData designData)
{
private IDesignEditor _editor;
private object _object;
public readonly EquipSlot Slot = slot;
private IDesignEditor _editor = null!;
private object _object = null!;
public readonly EquipSlot Slot = slot;
public bool Locked;
public bool DisplayApplication;
public bool AllowRevert;
@ -23,29 +24,31 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
public readonly void SetItem(EquipItem item)
=> _editor.ChangeItem(_object, Slot, item, ApplySettings.Manual);
public readonly void SetStain(StainId stain)
=> _editor.ChangeStain(_object, Slot, stain, ApplySettings.Manual);
public readonly void SetStains(StainIds stains)
=> _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)
{
var manager = (DesignManager)_editor;
var design = (Design)_object;
manager.ChangeApplyItem(design, Slot, value);
manager.ChangeApplyItem((Design)_object, Slot, value);
}
public readonly void SetApplyStain(bool value)
{
var manager = (DesignManager)_editor;
var design = (Design)_object;
manager.ChangeApplyStain(design, Slot, value);
manager.ChangeApplyStains((Design)_object, Slot, value);
}
public EquipItem CurrentItem = designData.Item(slot);
public StainId CurrentStain = designData.Stain(slot);
public EquipItem GameItem = default;
public StainId GameStain = default;
public EquipItem CurrentItem = designData.Item(slot);
public StainIds CurrentStains = designData.Stain(slot);
public EquipItem GameItem = default;
public StainIds GameStains = default;
public bool CurrentApply;
public bool CurrentApplyStain;
public bool HasAdvancedDyes;
public readonly Gender CurrentGender = designData.Customize.Gender;
public readonly Race CurrentRace = designData.Customize.Race;
@ -58,6 +61,7 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
CurrentApply = design.DoApplyEquip(slot),
CurrentApplyStain = design.DoApplyStain(slot),
Locked = design.WriteProtected(),
HasAdvancedDyes = design.GetMaterialDataRef().CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)),
DisplayApplication = true,
};
@ -69,7 +73,8 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
Locked = state.IsLocked,
DisplayApplication = false,
GameItem = state.BaseData.Item(slot),
GameStain = state.BaseData.Stain(slot),
GameStains = state.BaseData.Stain(slot),
HasAdvancedDyes = state.Materials.CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)),
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.Services;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using Dalamud.Bindings.ImGui;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -23,29 +26,35 @@ public class EquipmentDrawer
private readonly GlamourerColorCombo _stainCombo;
private readonly DictStain _stainData;
private readonly ItemCombo[] _itemCombo;
private readonly BonusItemCombo[] _bonusItemCombo;
private readonly Dictionary<FullEquipType, WeaponCombo> _weaponCombo;
private readonly CodeService _codes;
private readonly TextureService _textures;
private readonly Configuration _config;
private readonly GPoseService _gPose;
private readonly AdvancedDyePopup _advancedDyes;
private readonly ItemCopyService _itemCopy;
private float _requiredComboWidthUnscaled;
private float _requiredComboWidth;
public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, CodeService codes, TextureService textures,
Configuration config, GPoseService gPose, AdvancedDyePopup advancedDyes)
private Stain? _draggedStain;
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;
_codes = codes;
_textures = textures;
_config = config;
_gPose = gPose;
_advancedDyes = advancedDyes;
_stainData = items.Stains;
_stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites);
_itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray();
_weaponCombo = new Dictionary<FullEquipType, WeaponCombo>(FullEquipTypeExtensions.WeaponTypes.Count * 2);
_items = items;
_textures = textures;
_config = config;
_gPose = gPose;
_advancedDyes = advancedDyes;
_itemCopy = itemCopy;
_stainData = items.Stains;
_stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites);
_itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray();
_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>())
{
if (type.ToSlot() is EquipSlot.MainHand)
@ -59,6 +68,7 @@ public class EquipmentDrawer
private Vector2 _iconSize;
private float _comboLength;
private uint _advancedMaterialColor;
public void Prepare()
{
@ -70,7 +80,9 @@ public class EquipmentDrawer
.Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X)
/ ImGuiHelpers.GlobalScale;
_requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale;
_requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale;
_advancedMaterialColor = ColorId.AdvancedDyeActive.Value();
_dragTarget = EquipSlot.Unknown;
}
private bool VerifyRestrictedGear(EquipDrawData data)
@ -87,21 +99,34 @@ public class EquipmentDrawer
if (_config.HideApplyCheckmarks)
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 };
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
if (_config.SmallEquip)
DrawEquipSmall(equipDrawData);
else if (!equipDrawData.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan))
DrawEquipArtisan(equipDrawData);
else
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)
{
if (mainhand.CurrentItem.PrimaryId.Id == 0)
if (mainhand.CurrentItem.PrimaryId.Id == 0 && !allWeapons)
return;
if (_config.HideApplyCheckmarks)
@ -110,14 +135,12 @@ public class EquipmentDrawer
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 };
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
if (_config.SmallEquip)
DrawWeaponsSmall(mainhand, offhand, allWeapons);
else if (!mainhand.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan))
DrawWeaponsArtisan(mainhand, offhand);
else
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);
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 (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out var stain))
ret = stain.RowIndex;
ret = StainIds.All(stain.RowIndex);
else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex)
ret = Stain.None.RowIndex;
ret = StainIds.None;
if (!locked)
{
if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _config.DeleteDesignModifier.IsActive())
{
ret = Stain.None.RowIndex;
ret = StainIds.None;
change = true;
}
ImGuiUtil.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear.");
ImUtf8.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear.");
}
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
@ -287,14 +205,31 @@ public class EquipmentDrawer
}
else if (equipDrawData.IsState)
{
_advancedDyes.DrawButton(equipDrawData.Slot);
_advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u);
}
if (VerifyRestrictedGear(equipDrawData))
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.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)
@ -311,12 +246,12 @@ public class EquipmentDrawer
}
else if (mainhand.IsState)
{
_advancedDyes.DrawButton(EquipSlot.MainHand);
_advancedDyes.DrawButton(EquipSlot.MainHand, mainhand.HasAdvancedDyes ? _advancedMaterialColor : 0u);
}
if (allWeapons)
mainhandLabel += $" ({mainhand.CurrentItem.Type.ToName()})";
WeaponHelpMarker(mainhandLabel);
WeaponHelpMarker(mainhand is { IsDesign: true, HasAdvancedDyes: true }, mainhandLabel);
if (offhand.CurrentItem.Type is FullEquipType.Unknown)
return;
@ -333,10 +268,10 @@ public class EquipmentDrawer
}
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
@ -357,8 +292,8 @@ public class EquipmentDrawer
DrawApply(equipDrawData);
}
ImGui.SameLine();
ImGui.TextUnformatted(label);
DrawEquipLabel(equipDrawData is { IsDesign: true, HasAdvancedDyes: true }, label);
DrawStain(equipDrawData, false);
if (equipDrawData.DisplayApplication)
{
@ -367,16 +302,36 @@ public class EquipmentDrawer
}
else if (equipDrawData.IsState)
{
_advancedDyes.DrawButton(equipDrawData.Slot);
_advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u);
}
if (VerifyRestrictedGear(equipDrawData))
{
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)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
@ -385,7 +340,7 @@ public class EquipmentDrawer
mainhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.MainHand);
var left = ImGui.IsItemClicked(ImGuiMouseButton.Left);
ImGui.SameLine();
using (ImRaii.Group())
using (ImUtf8.Group())
{
DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, false, left);
if (mainhand.DisplayApplication)
@ -394,7 +349,8 @@ public class EquipmentDrawer
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);
if (mainhand.DisplayApplication)
@ -404,7 +360,7 @@ public class EquipmentDrawer
}
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);
left = ImGui.IsItemClicked(ImGuiMouseButton.Left);
ImGui.SameLine();
using (ImRaii.Group())
using (ImUtf8.Group())
{
DrawOffhand(mainhand, offhand, out var offhandLabel, false, right, left);
if (offhand.DisplayApplication)
@ -424,7 +380,7 @@ public class EquipmentDrawer
DrawApply(offhand);
}
WeaponHelpMarker(offhandLabel);
WeaponHelpMarker(offhand is { IsDesign: true, HasAdvancedDyes: true }, offhandLabel);
DrawStain(offhand, false);
if (offhand.DisplayApplication)
@ -432,28 +388,62 @@ public class EquipmentDrawer
ImGui.SameLine();
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)
{
var found = _stainData.TryGetValue(data.CurrentStain, out var stain);
using var disabled = ImRaii.Disabled(data.Locked);
var change = small
? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss)
: _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength);
if (change)
if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain))
data.SetStain(stain.RowIndex);
else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex)
data.SetStain(Stain.None.RowIndex);
var width = (_comboLength - ImUtf8.ItemInnerSpacing.X * (data.CurrentStains.Count - 1)) / data.CurrentStains.Count;
foreach (var (stainId, index) in data.CurrentStains.WithIndex())
{
using var id = ImUtf8.PushId(index);
var found = _stainData.TryGetValue(stainId, out var stain);
var change = small
? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss)
: _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 var newStain))
data.SetStain(newStain);
_itemCopy.HandleCopyPaste(data, index);
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)
@ -468,16 +458,91 @@ public class EquipmentDrawer
using var disabled = ImRaii.Disabled(data.Locked);
var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
_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)
data.SetItem(combo.CurrentSelection);
else if (combo.CustomVariant.Id > 0)
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))
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,
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, false, _) => ("Control and mouse wheel to scroll.", default, false),
};
ImGuiUtil.HoverTooltip(tt);
ImUtf8.HoverTooltip(tt);
return clicked && valid;
}
@ -526,8 +591,13 @@ public class EquipmentDrawer
if (combo.Draw(mainhand.CurrentItem.Name, mainhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
_requiredComboWidth))
changedItem = combo.CurrentSelection;
else if (ResetOrClear(mainhand.Locked || unknown, open, mainhand.AllowRevert, false, mainhand.CurrentItem, mainhand.GameItem,
default, out var c))
else if (combo.CustomVariant.Id > 0 && (drawAll || ItemData.ConvertWeaponId(combo.CustomSetId) == mainhand.CurrentItem.Type))
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;
if (changedItem != null)
@ -543,8 +613,9 @@ public class EquipmentDrawer
}
}
if (unknown && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("The weapon type could not be identified, thus changing it to other weapons of that type is not possible.");
if (unknown)
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)
@ -557,13 +628,17 @@ public class EquipmentDrawer
label = combo.Label;
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);
if (!locked && open)
UiHelpers.OpenCombo($"##{combo.Label}");
if (combo.Draw(offhand.CurrentItem.Name, offhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
_requiredComboWidth))
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);
if (ResetOrClear(locked, clear, offhand.AllowRevert, true, offhand.CurrentItem, offhand.GameItem, defaultOffhand, out var item))
@ -577,6 +652,13 @@ public class EquipmentDrawer
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)
{
if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this dye to the item when applying the Design.", data.CurrentApplyStain,
@ -587,14 +669,14 @@ public class EquipmentDrawer
#endregion
private static void WeaponHelpMarker(string label, string? type = null)
private void WeaponHelpMarker(bool hasAdvancedDyes, string label, string? type = null)
{
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"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.");
ImGui.SameLine();
ImGui.TextUnformatted(label);
DrawEquipLabel(hasAdvancedDyes, label);
if (type == null)
return;
@ -602,4 +684,17 @@ public class EquipmentDrawer
pos.Y += ImGui.GetFrameHeightWithSpacing();
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.Raii;
using Glamourer.Unlocks;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Widgets;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs;

View file

@ -1,12 +1,13 @@
using Dalamud.Plugin.Services;
using Glamourer.Services;
using Glamourer.Unlocks;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using Dalamud.Bindings.ImGui;
using Lumina.Excel.Sheets;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -75,32 +76,32 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
var ret = ImGui.Selectable(name, selected);
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
ImGuiUtil.RightAlign($"({obj.ModelString})");
ImUtf8.TextRightAligned($"({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());
=> base.IsVisible(globalIndex, filter) || Items[globalIndex].ModelString.StartsWith(filter.Lower);
protected override string ToString(EquipItem obj)
=> obj.Name;
private static string GetLabel(IDataManager gameData, EquipSlot slot)
{
var sheet = gameData.GetExcelSheet<Addon>()!;
var sheet = gameData.GetExcelSheet<Addon>();
return slot switch
{
EquipSlot.Head => sheet.GetRow(740)?.Text.ToString() ?? "Head",
EquipSlot.Body => sheet.GetRow(741)?.Text.ToString() ?? "Body",
EquipSlot.Hands => sheet.GetRow(742)?.Text.ToString() ?? "Hands",
EquipSlot.Legs => sheet.GetRow(744)?.Text.ToString() ?? "Legs",
EquipSlot.Feet => sheet.GetRow(745)?.Text.ToString() ?? "Feet",
EquipSlot.Ears => sheet.GetRow(746)?.Text.ToString() ?? "Ears",
EquipSlot.Neck => sheet.GetRow(747)?.Text.ToString() ?? "Neck",
EquipSlot.Wrists => sheet.GetRow(748)?.Text.ToString() ?? "Wrists",
EquipSlot.RFinger => sheet.GetRow(749)?.Text.ToString() ?? "Right Ring",
EquipSlot.LFinger => sheet.GetRow(750)?.Text.ToString() ?? "Left Ring",
EquipSlot.Head => sheet.TryGetRow(740, out var text) ? text.Text.ToString() : "Head",
EquipSlot.Body => sheet.TryGetRow(741, out var text) ? text.Text.ToString() : "Body",
EquipSlot.Hands => sheet.TryGetRow(742, out var text) ? text.Text.ToString() : "Hands",
EquipSlot.Legs => sheet.TryGetRow(744, out var text) ? text.Text.ToString() : "Legs",
EquipSlot.Feet => sheet.TryGetRow(745, out var text) ? text.Text.ToString() : "Feet",
EquipSlot.Ears => sheet.TryGetRow(746, out var text) ? text.Text.ToString() : "Ears",
EquipSlot.Neck => sheet.TryGetRow(747, out var text) ? text.Text.ToString() : "Neck",
EquipSlot.Wrists => sheet.TryGetRow(748, out var text) ? text.Text.ToString() : "Wrists",
EquipSlot.RFinger => sheet.TryGetRow(749, out var text) ? text.Text.ToString() : "Right Ring",
EquipSlot.LFinger => sheet.TryGetRow(750, out var text) ? text.Text.ToString() : "Left Ring",
_ => string.Empty,
};
}

View file

@ -0,0 +1,73 @@
using Glamourer.Services;
using Dalamud.Bindings.ImGui;
using OtterGui.Services;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
public class ItemCopyService(ItemManager items, DictStain stainData) : IUiService
{
public EquipItem? Item { get; private set; }
public Stain? Stain { get; private set; }
public void Copy(in EquipItem item)
=> Item = item;
public void Copy(in Stain stain)
=> Stain = stain;
public void Paste(int which, Action<int, StainId> setter)
{
if (Stain is { } stain)
setter(which, stain.RowIndex);
}
public void Paste(FullEquipType type, Action<EquipItem> setter)
{
if (Item is not { } item)
return;
if (type != item.Type)
{
if (type.IsBonus())
item = items.Identify(type.ToBonus(), item.PrimaryId, item.Variant);
else if (type.IsEquipment() || type.IsAccessory())
item = items.Identify(type.ToSlot(), item.PrimaryId, item.Variant);
else
item = items.Identify(type.ToSlot(), item.PrimaryId, item.SecondaryId, item.Variant);
}
if (item.Valid && item.Type == type)
setter(item);
}
public void HandleCopyPaste(in EquipDrawData data)
{
if (ImGui.GetIO().KeyCtrl)
{
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Middle))
Paste(data.CurrentItem.Type, data.SetItem);
}
else if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && ImGui.IsMouseClicked(ImGuiMouseButton.Middle))
{
Copy(data.CurrentItem);
}
}
public void HandleCopyPaste(in EquipDrawData data, int which)
{
if (ImGui.GetIO().KeyCtrl)
{
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Middle))
Paste(which, data.SetStain);
}
else if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)
&& ImGui.IsMouseClicked(ImGuiMouseButton.Middle)
&& stainData.TryGetValue(data.CurrentStains[which].Id, out var stain))
{
Copy(stain);
}
}
}

View file

@ -1,10 +1,11 @@
using Glamourer.Services;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using Dalamud.Bindings.ImGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -18,6 +19,10 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
private ItemId _currentItem;
private float _innerWidth;
public PrimaryId CustomSetId { get; private set; }
public SecondaryId CustomWeaponId { get; private set; }
public Variant CustomVariant { get; private set; }
public WeaponCombo(ItemManager items, FullEquipType type, Logger log, FavoriteManager favorites)
: base(() => GetWeapons(favorites, items, type), MouseWheelType.Control, log)
{
@ -45,8 +50,9 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
public bool Draw(string previewName, ItemId previewIdx, float width, float innerWidth)
{
_innerWidth = innerWidth;
_currentItem = previewIdx;
_innerWidth = innerWidth;
_currentItem = previewIdx;
CustomVariant = 0;
return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
}
@ -69,18 +75,36 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
var ret = ImGui.Selectable(name, selected);
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
ImGuiUtil.RightAlign($"({obj.PrimaryId.Id}-{obj.SecondaryId.Id}-{obj.Variant})");
ImUtf8.TextRightAligned($"({obj.PrimaryId.Id}-{obj.SecondaryId.Id}-{obj.Variant.Id})");
return ret;
}
protected override void OnClosePopup()
{
// If holding control while the popup closes, try to parse the input as a full tuple of set id, weapon 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 != 3
|| !ushort.TryParse(split[0], out var setId)
|| !ushort.TryParse(split[1], out var weaponId)
|| !byte.TryParse(split[2], out var variant))
return;
CustomSetId = setId;
CustomWeaponId = weaponId;
CustomVariant = variant;
}
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)
=> obj.Name;
private static string GetLabel(FullEquipType type)
=> type is FullEquipType.Unknown ? "Mainhand" : type.ToName();
=> type.IsUnknown() ? "Mainhand" : type.ToName();
private static IReadOnlyList<EquipItem> GetWeapons(FavoriteManager favorites, ItemManager items, FullEquipType type)
{

View file

@ -3,7 +3,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using Glamourer.Gui.Materials;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;

View file

@ -34,6 +34,17 @@ public class GlamourerChangelog
Add1_2_1_0(Changelog);
AddDummy(Changelog);
Add1_2_3_0(Changelog);
Add1_3_1_0(Changelog);
Add1_3_2_0(Changelog);
Add1_3_3_0(Changelog);
Add1_3_4_0(Changelog);
Add1_3_5_0(Changelog);
Add1_3_6_0(Changelog);
Add1_3_7_0(Changelog);
Add1_3_8_0(Changelog);
Add1_4_0_0(Changelog);
Add1_5_0_0(Changelog);
Add1_5_1_0(Changelog);
}
private (int, ChangeLogDisplayType) ConfigData()
@ -54,6 +65,209 @@ public class GlamourerChangelog
}
}
private static void Add1_5_1_0(Changelog log)
=> log.NextVersion("Version 1.5.1.0")
.RegisterHighlight("Added support for Penumbras PCP functionality to add the current state of the character as a design.")
.RegisterEntry("On import, a design for the PCP is created and, if possible, applied to the character.", 1)
.RegisterEntry("No automation is assigned.", 1)
.RegisterEntry("Finer control about this can be found in the settings.", 1)
.RegisterEntry("Fixed an issue with static visors not toggling through Glamourer (1.5.0.7).")
.RegisterEntry("The advanced dye slot combo now contains glasses (1.5.0.7).")
.RegisterEntry("Several fixes for patch-related issues (1.5.0.1 - 1.5.0.6");
private static void Add1_5_0_0(Changelog log)
=> log.NextVersion("Version 1.5.0.0")
.RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.")
.RegisterHighlight("Added the new Viera Ears state to designs. Old designs will not apply the state.")
.RegisterHighlight("Added the option to make newly created designs write-protected by default to the design defaults.")
.RegisterEntry("Fixed issues with reverting state and IPC.")
.RegisterEntry("Fixed an issue when using the mousewheel to scroll through designs (1.4.0.3).")
.RegisterEntry("Fixed an issue with invalid bonus items (1.4.0.3).")
.RegisterHighlight("Added drag & drop of equipment pieces which will try to match the corresponding model IDs in other slots if possible (1.4.0.2).")
.RegisterEntry("Heavily optimized some issues when having many designs and creating new ones or updating them (1.4.0.2)")
.RegisterEntry("Fixed an issue with staining templates (1.4.0.1).")
.RegisterEntry("Fixed an issue with the QDB buttons not counting correctly (1.4.0.1).");
private static void Add1_4_0_0(Changelog log)
=> log.NextVersion("Version 1.4.0.0")
.RegisterHighlight("The design selector width is now draggable within certain restrictions that depend on the total window width.")
.RegisterEntry("The current behavior may not be final, let me know if you have any comments.", 1)
.RegisterEntry("Regular customization colors can now be dragged & dropped onto other customizations.")
.RegisterEntry(
"If no identical color is available in the target slot, the most similar color available (for certain values of similar) will be chosen instead.",
1)
.RegisterEntry("Resetting advanced dyes and customizations has been split into two buttons for the quick design bar.")
.RegisterEntry("Weapons now also support custom ID input in the combo search box.")
.RegisterEntry("Added new IPC methods GetExtendedDesignData, AddDesign, DeleteDesign, GetDesignBase64, GetDesignJObject.")
.RegisterEntry("Added the option to prevent immediate repeats for random design selection (Thanks Diorik!).")
.RegisterEntry("Optimized some multi-design changes when selecting many designs and changing them at once.")
.RegisterEntry("Fixed item combos not starting from the currently selected item when scrolling them via mouse wheel.")
.RegisterEntry("Fixed some issue with Glamourer not searching mods by name for mod associations in some cases.")
.RegisterEntry("Fixed the IPC methods SetMetaState and SetMetaStateName not working (Thanks Caraxi!).")
.RegisterEntry("Added new IPC method GetDesignListExtended. (1.3.8.6)")
.RegisterEntry(
"Improved the naming of NPCs for identifiers by using Haselnussbombers new naming functionality (Thanks Hasel!). (1.3.8.6)")
.RegisterEntry(
"Added a modifier key separate from the delete modifier key that is used for less important key-checks, specifically toggling incognito mode. (1.3.8.5)")
.RegisterEntry("Used better Penumbra IPC for some things. (1.3.8.5)")
.RegisterEntry("Fixed an issue with advanced dyes for weapons. (1.3.8.5)")
.RegisterEntry("Fixed an issue with NPC automation due to missing job detection. (1.3.8.1)");
private static void Add1_3_8_0(Changelog log)
=> log.NextVersion("Version 1.3.8.0")
.RegisterImportant("Updated Glamourer for update 7.20 and Dalamud API 12.")
.RegisterEntry(
"This is not thoroughly tested, but I decided to push to stable instead of testing because otherwise a lot of people would just go to testing just for early access again despite having no business doing so.",
1)
.RegisterEntry(
"I also do not use most of the functionality of Glamourer myself, so I am unable to even encounter most issues myself.", 1)
.RegisterEntry("If you encounter any issues, please report them quickly on the discord.", 1)
.RegisterEntry("Added a chat command to clear temporary settings applied by Glamourer to Penumbra.")
.RegisterEntry("Fixed small issues with customizations not applicable to your race still applying.");
private static void Add1_3_7_0(Changelog log)
=> log.NextVersion("Version 1.3.7.0")
.RegisterImportant(
"The option to disable advanced customizations or advanced dyes has been removed. The functionality can no longer be disabled entirely, you can just decide not to use it, and to hide it.")
.RegisterHighlight(
"You can now configure which panels (like Customization, Equipment, Advanced Customization etc.) are displayed at all, and which are expanded by default. This does not disable any functionality.")
.RegisterHighlight(
"The Unlocks tab now shows whether items are modded in the currently selected collection in Penumbra in Overview mode and shows and can filter and sort for it in Detailed mode.")
.RegisterEntry("Added an optional button to the Quick Design Bar to reset all temporary settings applied by Glamourer.")
.RegisterHighlight(
"Any existing advanced dyes will now be highlighted on the corresponding Advanced Dye buttons in the actors panel and on the corresponding equip slot name in the design panel.")
.RegisterEntry("This also affects currently inactive advanced dyes, which can now be manually removed on the inactive materials.",
1)
.RegisterHighlight(
"In the design list of an automation set, the design indices are now highlighted if a design contains advanced dyes, mod associations, or links to other designs.")
.RegisterHighlight("Some quality of life improvements:")
.RegisterEntry("Added some buttons for some application rule presets to the Application Rules panel.", 1)
.RegisterEntry("Added some buttons to enable, disable or delete all advanced dyes in a design.", 1)
.RegisterEntry("Some of those buttons are also available in multi-design selection to apply to all selected designs at once.", 1)
.RegisterEntry(
"A copied material color set from Penumbra should now be able to be imported into a advanced dye color set, as well as the other way around.")
.RegisterEntry(
"Automatically applied character updates when applying a design with mod associations and temporary settings are now skipped to prevent some issues with GPose. This should not affect anything else.")
.RegisterEntry("Glamourer now differentiates between temporary settings applied through manual or automatic application.");
private static void Add1_3_6_0(Changelog log)
=> log.NextVersion("Version 1.3.6.0")
.RegisterHighlight("Added some new multi design selection functionality to change design settings of many designs at once.")
.RegisterEntry("Also added the number of selected designs and folders to the multi design selection display.", 1)
.RegisterEntry("Glamourer will now use temporary settings when saving mod associations, if they exist in Penumbra.")
.RegisterEntry(
"Actually added the checkbox to reset all temporary settings to Automation Sets (functionality was there, just not exposed to the UI...).")
.RegisterEntry(
"Adapted the behavior for identified copies of characters that have a different state than the character itself to deal with the associated Penumbra changes.")
.RegisterEntry(
"Added '/glamour resetdesign' as a command, that re-applies automation but resets randomly chosen designs (Thanks Diorik).")
.RegisterEntry("All existing facepaints should now be accepted in designs, including NPC facepaints.")
.RegisterEntry(
"Overwriting a design with your characters current state will now discard any prior advanced dyes and only add those from the current state.")
.RegisterEntry("Fixed an issue with racial mount and accessory scaling when changing zones on a changed race.")
.RegisterEntry("Fixed issues with the detection of gear set changes in certain circumstances (Thanks Cordelia).")
.RegisterEntry("Fixed an issue with the Force to Inherit checkbox in mod associations.")
.RegisterEntry(
"Added a new IPC event that fires only when Glamourer finalizes its current changes to a character (for/from Cordelia).")
.RegisterEntry("Added new IPC to set a meta flag on actors. (for/from Cordelia).");
private static void Add1_3_5_0(Changelog log)
=> log.NextVersion("Version 1.3.5.0")
.RegisterHighlight(
"Added the usage of the new Temporary Mod Setting functionality from Penumbra to apply mod associations. This is on by default but can be turned back to permanent changes in the settings.")
.RegisterEntry("Designs now have a setting to always reset all prior temporary settings made by Glamourer on application.", 1)
.RegisterEntry("Automation Sets also have a setting to do this, independently of the designs contained in them.", 1)
.RegisterHighlight("More NPC customization options should now be accepted as valid for designs, regardless of clan/gender.")
.RegisterHighlight("The 'Apply' chat command had the currently selected design and the current quick bar design added as choices.")
.RegisterEntry(
"The application buttons for designs, NPCs or actors should now stick at the top of their respective panels even when scrolling down.")
.RegisterHighlight("Randomly chosen designs should now stay across loading screens or redrawing. (1.3.4.3)")
.RegisterEntry(
"In automation, Random designs now have an option to always choose another design, including during loading screens or redrawing.",
1)
.RegisterEntry("Fixed an issue where disabling auto designs did not work as expected.")
.RegisterEntry("Fixed the inversion of application flags in IPC calls.")
.RegisterEntry("Fixed an issue with the scaling of the Advanced Dye popup with increased font sizes.")
.RegisterEntry("Fixed a bug when editing gear set conditions in the automation tab.")
.RegisterEntry("Fixed some ImGui issues.");
private static void Add1_3_4_0(Changelog log)
=> log.NextVersion("Version 1.3.4.0")
.RegisterEntry("Glamourer has been updated for Dalamud API 11 and patch 7.1.")
.RegisterEntry("Maybe fixed issues with shared weapon types and reset designs.")
.RegisterEntry("Fixed issues with resetting advanced dyes and certain weapon types.");
private static void Add1_3_3_0(Changelog log)
=> log.NextVersion("Version 1.3.3.0")
.RegisterHighlight("Added the option to create automations for owned human NPCs (like trust avatars).")
.RegisterEntry("Added some special filters to the Actors tab selector, hover over it to see the options.")
.RegisterEntry("Added an option for designs to always reset all previously applied advanced dyes.")
.RegisterEntry("Added some new NPC-only customizations to the valid customizations.")
.RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke.");
private static void Add1_3_2_0(Changelog log)
=> log.NextVersion("Version 1.3.2.0")
.RegisterEntry("Fixed an issue with weapon hiding when leaving GPose or changing zones.")
.RegisterEntry("Added support for unnamed items to be previewed from Penumbra.")
.RegisterEntry(
"Item combos filters now check if the model string starts with the current filter, instead of checking if the primary ID contains the current filter.")
.RegisterEntry("Improved the handling of bonus items (glasses) in designs.")
.RegisterEntry("Imported .chara files now import bonus items.")
.RegisterEntry(
"Added a Debug Data rider in the Actors tab that is visible if Debug Mode is enabled and (currently) contains some IDs.")
.RegisterEntry("Fixed bonus items not reverting correctly in some cases.")
.RegisterEntry("Fixed an issue with the RNG in cheat codes and events skipping some possible entries.")
.RegisterEntry("Fixed the chat log context menu for glamourer Try-On.")
.RegisterEntry("Fixed some issues with cheat code sets.")
.RegisterEntry(
"Made the popped out Advanced Dye Window and Unlocks Window non-docking as that caused issues when docked to the main Glamourer window.")
.RegisterEntry("Refreshed NPC name associations.")
.RegisterEntry("Removed a now useless cheat code.")
.RegisterEntry("Added API for Bonus Items. (1.3.1.1)");
private static void Add1_3_1_0(Changelog log)
=> log.NextVersion("Version 1.3.1.0")
.RegisterHighlight("Glamourer is now released for Dawntrail!")
.RegisterEntry("Added support for female Hrothgar.", 1)
.RegisterEntry("Added support for the Glasses slot.", 1)
.RegisterEntry("Added support for two dye slots.", 1)
.RegisterImportant(
"There were some issues with Advanced Dyes stored in Designs. When launching this update, Glamourer will try to migrate all your old designs into the new form.")
.RegisterEntry("Unfortunately, this is slightly based on guesswork and may cause false-positive migrations.", 1)
.RegisterEntry("In general, the values for Gloss and Specular Strength were swapped, so the migration swaps them back.", 1)
.RegisterEntry(
"In some cases this may not be correct, or the values stored were problematic to begin with and will now cause further issues.",
1)
.RegisterImportant(
"If your designs lose their specular color, you need to verify that the Specular Strength is non-zero (usually in 0-100%).", 1)
.RegisterImportant(
"If your designs are extremely glossy and reflective, you need to verify that the Gloss value is greater than zero (usually a power of 2 >= 1, it should never be 0).",
1)
.RegisterEntry(
"I am very sorry for the inconvenience but there is no way to salvage this sanely in all cases, especially with user-input values.",
1)
.RegisterImportant(
"Any materials already using Dawntrails shaders will currently not be able to edit the Gloss or Specular Strength Values in Advanced Dyes.")
.RegisterImportant(
"Skin and Hair Shine from advanced customizations are not supported by the game any longer, so they are not displayed for the moment.")
.RegisterHighlight("All eyes now support Limbal rings (which use the Feature Color for their color).")
.RegisterHighlight("Dyes can now be dragged and dropped onto other dyes to replicate them.")
.RegisterEntry("The job filter in the Unlocks tab has been improved.")
.RegisterHighlight(
"Editing designs or actors now has a history and you can undo up to 16 of the last changes you made, separately per design or actor.")
.RegisterEntry(
"Some changes (like when a weapon applies its offhand) may count as multiple and have to be stepped back separately.", 1)
.RegisterEntry("You can now change the priority or enabled state of associated mods directly.")
.RegisterEntry("Glamourer now has a Support Info button akin to Penumbra's.")
.RegisterEntry("Glamourer now respects write protection on designs better.")
.RegisterEntry("The advanced dye window popup should now get focused when it is opening even in detached state.")
.RegisterEntry("Added API and IPC for bonus items, i.e. the Glasses slot.")
.RegisterHighlight("You can now display your characters height in Corgis or Olympic Swimming Pools.")
.RegisterEntry("Fixed some issues with advanced customizations and dyes applied via IPC. (1.2.3.2)")
.RegisterEntry(
"Glamourer now uses the last matching game object for advanced dyes instead of the first (mainly relevant for GPose). (1.2.3.1)");
private static void Add1_2_3_0(Changelog log)
=> log.NextVersion("Version 1.2.3.0")
.RegisterHighlight(

View file

@ -7,11 +7,11 @@ namespace Glamourer.Gui;
public class GlamourerWindowSystem : IDisposable
{
private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly UiBuilder _uiBuilder;
private readonly IUiBuilder _uiBuilder;
private readonly MainWindow _ui;
private readonly PenumbraChangedItemTooltip _penumbraTooltip;
public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui, GenericPopupWindow popups, PenumbraChangedItemTooltip penumbraTooltip,
public GlamourerWindowSystem(IUiBuilder uiBuilder, MainWindow ui, GenericPopupWindow popups, PenumbraChangedItemTooltip penumbraTooltip,
Configuration config, UnlocksTab unlocksTab, GlamourerChangelog changelog, DesignQuickBar quick)
{
_uiBuilder = uiBuilder;

View file

@ -1,4 +1,5 @@
using Dalamud.Interface.Windowing;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Glamourer.Designs;
using Glamourer.Events;
@ -11,8 +12,9 @@ using Glamourer.Gui.Tabs.NpcTab;
using Glamourer.Gui.Tabs.SettingsTab;
using Glamourer.Gui.Tabs.UnlocksTab;
using Glamourer.Interop.Penumbra;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Custom;
using OtterGui.Raii;
using OtterGui.Services;
@ -62,7 +64,7 @@ public class MainWindow : Window, IDisposable
public TabType SelectTab;
public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs,
public MainWindow(IDalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs,
DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar,
NpcTab npcs, MainWindowPosition position, PenumbraService penumbra)
: base("GlamourerMainWindow")
@ -100,6 +102,8 @@ public class MainWindow : Window, IDisposable
SelectTab = _config.Ephemeral.SelectedTab;
_event.Subscribe(OnTabSelected, TabSelected.Priority.MainWindow);
IsOpen = _config.OpenWindowAtStart;
_penumbra.DrawSettingsSection += Settings.DrawPenumbraIntegrationSettings;
}
public void OpenSettings()
@ -118,7 +122,10 @@ public class MainWindow : Window, IDisposable
}
public void Dispose()
=> _event.Unsubscribe(OnTabSelected);
{
_event.Unsubscribe(OnTabSelected);
_penumbra.DrawSettingsSection -= Settings.DrawPenumbraIntegrationSettings;
}
public override void Draw()
{
@ -131,7 +138,12 @@ public class MainWindow : Window, IDisposable
if (_penumbra.CurrentMajor == 0)
DrawProblemWindow(
"Could not attach to Penumbra. Please make sure Penumbra is installed and running.\n\nPenumbra is required for Glamourer to work properly.");
else if (_penumbra is { CurrentMajor: PenumbraService.RequiredPenumbraBreakingVersion, CurrentMinor: >= PenumbraService.RequiredPenumbraFeatureVersion })
else if (_penumbra is
{
CurrentMajor: PenumbraService.RequiredPenumbraBreakingVersion,
CurrentMinor: >= PenumbraService.RequiredPenumbraFeatureVersion,
})
DrawProblemWindow(
$"You are currently not attached to Penumbra, seemingly by manually detaching from it.\n\nPenumbra's last API Version was {_penumbra.CurrentMajor}.{_penumbra.CurrentMinor}.\n\nPenumbra is required for Glamourer to work properly.");
else
@ -184,22 +196,42 @@ public class MainWindow : Window, IDisposable
return TabType.None;
}
/// <summary> The longest support button text. </summary>
public static ReadOnlySpan<byte> SupportInfoButtonText
=> "Copy Support Info to Clipboard"u8;
/// <summary> Draw the support button group on the right-hand side of the window. </summary>
public static void DrawSupportButtons(Changelog changelog)
public static void DrawSupportButtons(Glamourer glamourer, Changelog changelog)
{
var width = ImGui.CalcTextSize("Join Discord for Support").X + ImGui.GetStyle().FramePadding.X * 2;
var width = ImUtf8.CalcTextSize(SupportInfoButtonText).X + ImGui.GetStyle().FramePadding.X * 2;
var xPos = ImGui.GetWindowWidth() - width;
ImGui.SetCursorPos(new Vector2(xPos, 0));
CustomGui.DrawDiscordButton(Glamourer.Messager, width);
ImGui.SetCursorPos(new Vector2(xPos, ImGui.GetFrameHeightWithSpacing()));
CustomGui.DrawGuideButton(Glamourer.Messager, width);
DrawSupportButton(glamourer);
ImGui.SetCursorPos(new Vector2(xPos, 2 * ImGui.GetFrameHeightWithSpacing()));
CustomGui.DrawGuideButton(Glamourer.Messager, width);
ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing()));
if (ImGui.Button("Show Changelogs", new Vector2(width, 0)))
changelog.ForceOpen = true;
}
/// <summary>
/// Draw a button that copies the support info to clipboards.
/// </summary>
private static void DrawSupportButton(Glamourer glamourer)
{
if (!ImUtf8.Button(SupportInfoButtonText))
return;
var text = glamourer.GatherSupportInformation();
ImGui.SetClipboardText(text);
Glamourer.Messager.NotificationMessage("Copied Support Info to Clipboard.", NotificationType.Success, false);
}
private void OnTabSelected(TabType type, Design? _)
{
SelectTab = type;

View file

@ -1,19 +1,23 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using Glamourer.Designs;
using Glamourer.Interop.Material;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Interop;
using Penumbra.String;
using Notification = OtterGui.Classes.Notification;
namespace Glamourer.Gui.Materials;
@ -26,14 +30,16 @@ public sealed unsafe class AdvancedDyePopup(
private MaterialValueIndex? _drawIndex;
private ActorState _state = null!;
private Actor _actor;
private ColorRow.Mode _mode;
private byte _selectedMaterial = byte.MaxValue;
private bool _anyChanged;
private bool _forceFocus;
private const int RowsPerPage = 16;
private int _rowOffset;
private bool ShouldBeDrawn()
{
if (!config.UseAdvancedDyes)
return false;
if (_drawIndex is not { Valid: true })
return false;
@ -43,59 +49,69 @@ public sealed unsafe class AdvancedDyePopup(
return true;
}
public void DrawButton(EquipSlot slot)
=> DrawButton(MaterialValueIndex.FromSlot(slot));
public void DrawButton(EquipSlot slot, uint color)
=> DrawButton(MaterialValueIndex.FromSlot(slot), color);
private void DrawButton(MaterialValueIndex index)
public void DrawButton(BonusItemFlag slot, uint color)
=> DrawButton(MaterialValueIndex.FromSlot(slot), color);
private void DrawButton(MaterialValueIndex index, uint color)
{
if (!config.UseAdvancedDyes)
if (config.HideDesignPanel.HasFlag(DesignPanelFlag.AdvancedDyes))
return;
ImGui.SameLine();
using var id = ImRaii.PushId(index.SlotIndex | ((int)index.DrawObject << 8));
using var id = ImUtf8.PushId(index.SlotIndex | ((int)index.DrawObject << 8));
var isOpen = index == _drawIndex;
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), isOpen)
.Push(ImGuiCol.Text, ColorId.HeaderButtons.Value(), isOpen)
.Push(ImGuiCol.Border, ColorId.HeaderButtons.Value(), isOpen))
var (textColor, buttonColor) = isOpen
? (ColorId.HeaderButtons.Value(), ImGui.GetColorU32(ImGuiCol.ButtonActive))
: (color, 0u);
using (ImRaii.PushColor(ImGuiCol.Border, textColor, isOpen))
{
using var frame = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, isOpen);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Palette.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
string.Empty, false, true))
if (ImUtf8.IconButton(FontAwesomeIcon.Palette, ""u8, default, false, textColor, buttonColor))
{
_forceFocus = true;
_selectedMaterial = byte.MaxValue;
_drawIndex = isOpen ? null : index;
}
}
ImGuiUtil.HoverTooltip("Open advanced dyes for this slot.");
ImUtf8.HoverTooltip("Open advanced dyes for this slot."u8);
}
private (string Path, string GamePath) ResourceName(MaterialValueIndex index)
{
var materialHandle = (MaterialResourceHandle*)_actor.Model.AsCharacterBase->MaterialsSpan[
index.MaterialIndex + index.SlotIndex * MaterialService.MaterialsPerModel].Value;
var materialHandle =
(MaterialResourceHandle*)_actor.Model.AsCharacterBase->MaterialsSpan[
index.MaterialIndex + index.SlotIndex * MaterialService.MaterialsPerModel].Value;
var model = _actor.Model.AsCharacterBase->ModelsSpan[index.SlotIndex].Value;
var modelHandle = model == null ? null : model->ModelResourceHandle;
var path = materialHandle == null
? string.Empty
: ByteString.FromSpanUnsafe(materialHandle->ResourceHandle.FileName.AsSpan(), true).ToString();
: ByteString.FromSpanUnsafe(materialHandle->FileName.AsSpan(), true).ToString();
var gamePath = modelHandle == null
? string.Empty
: modelHandle->GetMaterialFileNameBySlotAsString(index.MaterialIndex);
: modelHandle->GetMaterialFileNameBySlot(index.MaterialIndex).ToString();
return (path, gamePath);
}
private void DrawTabBar(ReadOnlySpan<Pointer<Texture>> textures, ref bool firstAvailable)
private void DrawTabBar(ReadOnlySpan<Pointer<Texture>> textures, ReadOnlySpan<Pointer<Material>> materials, ref bool firstAvailable)
{
using var bar = ImRaii.TabBar("tabs");
using var bar = ImUtf8.TabBar("tabs"u8);
if (!bar)
return;
var table = new ColorTable.Table();
var highLightColor = ColorId.AdvancedDyeActive.Value();
for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i)
{
var index = _drawIndex!.Value with { MaterialIndex = i };
var available = index.TryGetTexture(textures, out var texture) && directX.TryGetColorTable(*texture, out var table);
var index = _drawIndex!.Value with { MaterialIndex = i };
var available = index.TryGetTexture(textures, materials, out var texture, out _mode)
&& directX.TryGetColorTable(*texture, out table);
if (index == preview.LastValueIndex with { RowIndex = 0 })
table = preview.LastOriginalColorTable;
@ -108,50 +124,78 @@ public sealed unsafe class AdvancedDyePopup(
if (available)
firstAvailable = false;
using var tab = _label.TabItem(i, select);
var hasAdvancedDyes = _state.Materials.CheckExistenceMaterial(index);
using var c = ImRaii.PushColor(ImGuiCol.Text, highLightColor, hasAdvancedDyes);
using var tab = _label.TabItem(i, select);
c.Pop();
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
using var enabled = ImRaii.Enabled();
var (path, gamePath) = ResourceName(index);
using var tt = ImUtf8.Tooltip();
if (gamePath.Length == 0 || path.Length == 0)
ImGui.SetTooltip("This material does not exist.");
ImUtf8.Text("This material does not exist."u8);
else if (!available)
ImGui.SetTooltip($"This material does not have an associated color set.\n\n{gamePath}\n{path}");
ImUtf8.Text($"This material does not have an associated color set.\n\n{gamePath}\n{path}");
else
ImGui.SetTooltip($"{gamePath}\n{path}");
ImUtf8.Text($"{gamePath}\n{path}");
if (hasAdvancedDyes && !available)
{
ImUtf8.Text("\nRight-Click to remove ineffective advanced dyes."u8);
if (ImGui.IsMouseClicked(ImGuiMouseButton.Right))
for (byte row = 0; row < ColorTable.NumRows; ++row)
stateManager.ResetMaterialValue(_state, index with { RowIndex = row }, ApplySettings.Game);
}
}
if ((tab.Success || select is ImGuiTabItemFlags.SetSelected) && available)
{
_selectedMaterial = i;
DrawToggle();
DrawTable(index, table);
}
}
}
using (ImRaii.PushFont(UiBuilder.IconFont))
private void DrawToggle()
{
var buttonWidth = new Vector2(ImGui.GetContentRegionAvail().X / 2, 0);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.TabHovered));
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(_rowOffset == 0 ? ImGuiCol.TabActive : ImGuiCol.Tab)))
{
if (ImGui.TabItemButton($"{FontAwesomeIcon.Times.ToIconString()} ", ImGuiTabItemFlags.NoTooltip))
_drawIndex = null;
if (ToggleButton.ButtonEx("Row Pairs 1-8 ", buttonWidth, ImGuiButtonFlags.MouseButtonLeft, ImDrawFlags.RoundCornersLeft))
_rowOffset = 0;
}
ImGuiUtil.HoverTooltip("Close the advanced dye window.");
ImGui.SameLine(0, 0);
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(_rowOffset == RowsPerPage ? ImGuiCol.TabActive : ImGuiCol.Tab)))
{
if (ToggleButton.ButtonEx("Row Pairs 9-16", buttonWidth, ImGuiButtonFlags.MouseButtonLeft, ImDrawFlags.RoundCornersRight))
_rowOffset = RowsPerPage;
}
}
private void DrawContent(ReadOnlySpan<Pointer<Texture>> textures)
private void DrawContent(ReadOnlySpan<Pointer<Texture>> textures, ReadOnlySpan<Pointer<Material>> materials)
{
var firstAvailable = true;
DrawTabBar(textures, ref firstAvailable);
DrawTabBar(textures, materials, ref firstAvailable);
if (firstAvailable)
ImGui.TextUnformatted("No Editable Materials available.");
ImUtf8.Text("No Editable Materials available."u8);
}
private void DrawWindow(ReadOnlySpan<Pointer<Texture>> textures)
private void DrawWindow(ReadOnlySpan<Pointer<Texture>> textures, ReadOnlySpan<Pointer<Material>> materials)
{
var flags = ImGuiWindowFlags.NoFocusOnAppearing
| ImGuiWindowFlags.NoCollapse
| ImGuiWindowFlags.NoDecoration
| ImGuiWindowFlags.NoResize;
| ImGuiWindowFlags.NoResize
| ImGuiWindowFlags.NoDocking;
// Set position to the right of the main window when attached
// The downwards offset is implicit through child position.
@ -163,15 +207,26 @@ public sealed unsafe class AdvancedDyePopup(
flags |= ImGuiWindowFlags.NoMove;
}
var size = new Vector2(7 * ImGui.GetFrameHeight() + 3 * ImGui.GetStyle().ItemInnerSpacing.X + 300 * ImGuiHelpers.GlobalScale,
18 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + 2 * ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetNextWindowSize(size);
var width = 7 * ImGui.GetFrameHeight() // Buttons
+ 3 * ImGui.GetStyle().ItemSpacing.X // around text
+ 7 * ImGui.GetStyle().ItemInnerSpacing.X
+ 200 * ImGuiHelpers.GlobalScale // Drags
+ 7 * UiBuilder.MonoFont.GetCharAdvance(' ') * ImGuiHelpers.GlobalScale // Row
+ 2 * ImGui.GetStyle().WindowPadding.X;
var height = 19 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + 3 * ImGui.GetStyle().ItemSpacing.Y;
ImGui.SetNextWindowSize(new Vector2(width, height));
var window = ImGui.Begin("###Glamourer Advanced Dyes", flags);
if (ImGui.IsWindowAppearing() || _forceFocus)
{
ImGui.SetWindowFocus();
_forceFocus = false;
}
try
{
if (window)
DrawContent(textures);
DrawContent(textures, materials);
}
finally
{
@ -186,18 +241,22 @@ public sealed unsafe class AdvancedDyePopup(
if (!ShouldBeDrawn())
return;
if (_drawIndex!.Value.TryGetTextures(actor, out var textures))
DrawWindow(textures);
if (_drawIndex!.Value.TryGetTextures(actor, out var textures, out var materials))
DrawWindow(textures, materials);
}
private void DrawTable(MaterialValueIndex materialIndex, in LegacyColorTable table)
private void DrawTable(MaterialValueIndex materialIndex, ColorTable.Table table)
{
if (!materialIndex.Valid)
return;
using var disabled = ImRaii.Disabled(_state.IsLocked);
_anyChanged = false;
for (byte i = 0; i < LegacyColorTable.NumUsedRows; ++i)
for (byte i = 0; i < RowsPerPage; ++i)
{
var index = materialIndex with { RowIndex = i };
ref var row = ref table[i];
var actualI = (byte)(i + _rowOffset);
var index = materialIndex with { RowIndex = actualI };
ref var row = ref table[actualI];
DrawRow(ref row, index, table);
}
@ -205,31 +264,89 @@ public sealed unsafe class AdvancedDyePopup(
DrawAllRow(materialIndex, table);
}
private void DrawAllRow(MaterialValueIndex materialIndex, in LegacyColorTable table)
private static void CopyToClipboard(in ColorTable.Table table)
{
try
{
fixed (ColorTable.Table* ptr = &table)
{
var data = new ReadOnlySpan<byte>(ptr, sizeof(ColorTable.Table));
var base64 = Convert.ToBase64String(data);
ImGui.SetClipboardText(base64);
}
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not copy color table to clipboard:\n{ex}");
}
}
private static bool ImportFromClipboard(out ColorTable.Table table)
{
try
{
var base64 = ImGui.GetClipboardText();
if (base64.Length > 0)
{
var data = Convert.FromBase64String(base64);
if (sizeof(ColorTable.Table) <= data.Length)
{
table = new ColorTable.Table();
fixed (ColorTable.Table* tPtr = &table)
{
fixed (byte* ptr = data)
{
new ReadOnlySpan<byte>(ptr, sizeof(ColorTable.Table)).CopyTo(new Span<byte>(tPtr, sizeof(ColorTable.Table)));
return true;
}
}
}
}
if (ColorRowClipboard.IsTableSet)
{
table = ColorRowClipboard.Table;
return true;
}
}
catch (Exception ex)
{
Glamourer.Messager.AddMessage(new Notification(ex, "Could not paste color table from clipboard.",
"Could not paste color table from clipboard.", NotificationType.Error));
}
table = default;
return false;
}
private void DrawAllRow(MaterialValueIndex materialIndex, in ColorTable.Table table)
{
using var id = ImRaii.PushId(100);
var buttonSize = new Vector2(ImGui.GetFrameHeight());
ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), buttonSize, "Highlight all affected colors on the character.",
false, true);
ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight all affected colors on the character."u8, buttonSize);
if (ImGui.IsItemHovered())
preview.OnHover(materialIndex with { RowIndex = byte.MaxValue }, _actor.Index, table);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TextUnformatted("All Color Rows");
ImUtf8.Text("All Color Row Pairs (1-16)"u8);
}
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
ImGui.SameLine(ImGui.GetWindowSize().X - 3 * buttonSize.X - 3 * spacing);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), buttonSize, "Export this table to your clipboard.", false,
true))
ImGui.SameLine(ImGui.GetWindowSize().X - 3 * buttonSize.X - 2 * spacing - ImGui.GetStyle().WindowPadding.X);
if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this table to your clipboard."u8, buttonSize))
{
ColorRowClipboard.Table = table;
CopyToClipboard(table);
}
ImGui.SameLine(0, spacing);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), buttonSize,
"Import an exported table from your clipboard onto this table.", !ColorRowClipboard.IsTableSet, true))
foreach (var (row, idx) in ColorRowClipboard.Table.WithIndex())
if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported table from your clipboard onto this table."u8, buttonSize)
&& ImportFromClipboard(out var newTable))
for (var idx = 0; idx < ColorTable.NumRows; ++idx)
{
var row = newTable[idx];
var internalRow = new ColorRow(row);
var slot = materialIndex.ToEquipSlot();
var weapon = slot is EquipSlot.MainHand or EquipSlot.OffHand
@ -240,23 +357,27 @@ public sealed unsafe class AdvancedDyePopup(
}
ImGui.SameLine(0, spacing);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UndoAlt.ToIconString(), buttonSize, "Reset this table to game state.", !_anyChanged,
true))
for (byte i = 0; i < LegacyColorTable.NumUsedRows; ++i)
if (ImUtf8.IconButton(FontAwesomeIcon.UndoAlt, "Reset this table to game state."u8, buttonSize, !_anyChanged))
for (byte i = 0; i < ColorTable.NumRows; ++i)
stateManager.ResetMaterialValue(_state, materialIndex with { RowIndex = i }, ApplySettings.Game);
}
private void DrawRow(ref LegacyColorTable.Row row, MaterialValueIndex index, in LegacyColorTable table)
private void DrawRow(ref ColorTableRow row, MaterialValueIndex index, in ColorTable.Table table)
{
using var id = ImRaii.PushId(index.RowIndex);
using var id = ImUtf8.PushId(index.RowIndex);
var changed = _state.Materials.TryGetValue(index, out var value);
if (!changed)
{
var internalRow = new ColorRow(row);
var slot = index.ToEquipSlot();
var weapon = slot is EquipSlot.MainHand or EquipSlot.OffHand
? _state.ModelData.Weapon(slot)
: _state.ModelData.Armor(slot).ToWeapon(0);
var weapon = slot switch
{
EquipSlot.MainHand => _state.ModelData.Weapon(EquipSlot.MainHand),
EquipSlot.OffHand => _state.ModelData.Weapon(EquipSlot.OffHand),
EquipSlot.Unknown =>
_state.ModelData.BonusItem((index.SlotIndex - 16u).ToBonusSlot()).Armor().ToWeapon(0), // TODO: Handle better
_ => _state.ModelData.Armor(slot).ToWeapon(0),
};
value = new MaterialValueState(internalRow, internalRow, weapon, StateSource.Manual);
}
else
@ -266,8 +387,7 @@ public sealed unsafe class AdvancedDyePopup(
}
var buttonSize = new Vector2(ImGui.GetFrameHeight());
ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), buttonSize, "Highlight the affected colors on the character.",
false, true);
ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight the affected colors on the character."u8, buttonSize);
if (ImGui.IsItemHovered())
preview.OnHover(index, _actor.Index, table);
@ -275,58 +395,107 @@ public sealed unsafe class AdvancedDyePopup(
ImGui.AlignTextToFramePadding();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TextUnformatted($"Row {index.RowIndex + 1:D2}");
var rowIndex = index.RowIndex / 2 + 1;
var rowSuffix = (index.RowIndex & 1) == 0 ? 'A' : 'B';
ImUtf8.Text($"Row {rowIndex,2}{rowSuffix}");
}
ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X * 2);
var applied = ImGuiUtil.ColorPicker("##diffuse", "Change the diffuse value for this row.", value.Model.Diffuse,
v => value.Model.Diffuse = v, "D");
var applied = ImUtf8.ColorPicker("##diffuse"u8, "Change the diffuse value for this row."u8, value.Model.Diffuse,
v => value.Model.Diffuse = v, "D"u8);
var spacing = ImGui.GetStyle().ItemInnerSpacing;
ImGui.SameLine(0, spacing.X);
applied |= ImGuiUtil.ColorPicker("##specular", "Change the specular value for this row.", value.Model.Specular,
v => value.Model.Specular = v, "S");
applied |= ImUtf8.ColorPicker("##specular"u8, "Change the specular value for this row."u8, value.Model.Specular,
v => value.Model.Specular = v, "S"u8);
ImGui.SameLine(0, spacing.X);
applied |= ImGuiUtil.ColorPicker("##emissive", "Change the emissive value for this row.", value.Model.Emissive,
v => value.Model.Emissive = v, "E");
applied |= ImUtf8.ColorPicker("##emissive"u8, "Change the emissive value for this row."u8, value.Model.Emissive,
v => value.Model.Emissive = v, "E"u8);
ImGui.SameLine(0, spacing.X);
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
applied |= ImGui.DragFloat("##Gloss", ref value.Model.GlossStrength, 0.01f, 0.001f, float.MaxValue, "%.3f G")
&& value.Model.GlossStrength > 0;
ImGuiUtil.HoverTooltip("Change the gloss strength for this row.");
if (_mode is not ColorRow.Mode.Dawntrail)
{
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
applied |= DragGloss(ref value.Model.GlossStrength);
ImUtf8.HoverTooltip("Change the gloss strength for this row."u8);
}
else
{
ImGui.Dummy(new Vector2(100 * ImGuiHelpers.GlobalScale, 0));
}
ImGui.SameLine(0, spacing.X);
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
applied |= ImGui.DragFloat("##Specular Strength", ref value.Model.SpecularStrength, 0.01f, float.MinValue, float.MaxValue, "%.3f SS");
ImGuiUtil.HoverTooltip("Change the specular strength for this row.");
if (_mode is not ColorRow.Mode.Dawntrail)
{
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
applied |= DragSpecularStrength(ref value.Model.SpecularStrength);
ImUtf8.HoverTooltip("Change the specular strength for this row."u8);
}
else
{
ImGui.Dummy(new Vector2(100 * ImGuiHelpers.GlobalScale, 0));
}
ImGui.SameLine(0, spacing.X);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), buttonSize, "Export this row to your clipboard.", false,
true))
if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8, buttonSize))
ColorRowClipboard.Row = value.Model;
ImGui.SameLine(0, spacing.X);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), buttonSize,
"Import an exported row from your clipboard onto this row.", !ColorRowClipboard.IsSet, true))
if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, buttonSize,
!ColorRowClipboard.IsSet))
{
value.Model = ColorRowClipboard.Row;
applied = true;
}
ImGui.SameLine(0, spacing.X);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UndoAlt.ToIconString(), buttonSize, "Reset this row to game state.", !changed, true))
if (ImUtf8.IconButton(FontAwesomeIcon.UndoAlt, "Reset this row to game state."u8, buttonSize, !changed))
stateManager.ResetMaterialValue(_state, index, ApplySettings.Game);
if (applied)
stateManager.ChangeMaterialValue(_state, index, value, ApplySettings.Manual);
}
public static bool DragGloss(ref float value)
{
var tmp = value;
var minValue = ImGui.GetIO().KeyCtrl ? 0f : (float)Half.Epsilon;
if (!ImUtf8.DragScalar("##Gloss"u8, ref tmp, "%.1f G"u8, 0.001f, minValue, Math.Max(0.01f, 0.005f * value),
ImGuiSliderFlags.AlwaysClamp))
return false;
var tmp2 = Math.Clamp(tmp, minValue, (float)Half.MaxValue);
if (tmp2 == value)
return false;
value = tmp2;
return true;
}
public static bool DragSpecularStrength(ref float value)
{
var tmp = value * 100f;
if (!ImUtf8.DragScalar("##SpecularStrength"u8, ref tmp, "%.0f%% SS"u8, 0f, (float)Half.MaxValue * 100f, 0.05f,
ImGuiSliderFlags.AlwaysClamp))
return false;
var tmp2 = Math.Clamp(tmp, 0f, (float)Half.MaxValue * 100f) / 100f;
if (tmp2 == value)
return false;
value = tmp2;
return true;
}
private LabelStruct _label = new();
private struct LabelStruct
{
private fixed byte _label[12];
private fixed byte _label[5];
public ImRaii.IEndObject TabItem(byte materialIndex, ImGuiTabItemFlags flags)
{
_label[10] = (byte)('1' + materialIndex);
_label[4] = (byte)('A' + materialIndex);
fixed (byte* ptr = _label)
{
return ImRaii.TabItem(ptr, flags | ImGuiTabItemFlags.NoTooltip);
@ -335,17 +504,11 @@ public sealed unsafe class AdvancedDyePopup(
public LabelStruct()
{
_label[0] = (byte)'M';
_label[1] = (byte)'a';
_label[2] = (byte)'t';
_label[3] = (byte)'e';
_label[4] = (byte)'r';
_label[5] = (byte)'i';
_label[6] = (byte)'a';
_label[7] = (byte)'l';
_label[8] = (byte)' ';
_label[9] = (byte)'#';
_label[11] = 0;
_label[0] = (byte)'M';
_label[1] = (byte)'a';
_label[2] = (byte)'t';
_label[3] = (byte)' ';
_label[5] = 0;
}
}
}

View file

@ -5,14 +5,14 @@ namespace Glamourer.Gui.Materials;
public static class ColorRowClipboard
{
private static ColorRow _row;
private static LegacyColorTable _table;
private static ColorRow _row;
private static ColorTable.Table _table;
public static bool IsSet { get; private set; }
public static bool IsTableSet { get; private set; }
public static LegacyColorTable Table
public static ColorTable.Table Table
{
get => _table;
set

View file

@ -3,9 +3,10 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Glamourer.Designs;
using Glamourer.Interop.Material;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Gui;
@ -17,7 +18,6 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
public const float GlossWidth = 100;
public const float SpecularStrengthWidth = 125;
private EquipSlot _newSlot = EquipSlot.Head;
private int _newMaterialIdx;
private int _newRowIdx;
private MaterialValueIndex _newKey = MaterialValueIndex.FromSlot(EquipSlot.Head);
@ -33,7 +33,11 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
var colorWidth = 4 * _buttonSize.X
+ (GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale
+ 6 * _spacing
+ ImGui.CalcTextSize("Revert").X;
+ ImUtf8.CalcTextSize("Revert"u8).X;
DrawMultiButtons(design);
ImUtf8.Dummy(0);
ImGui.Separator();
ImUtf8.Dummy(0);
if (available > 1.95 * colorWidth)
DrawSingleRow(design);
else
@ -41,11 +45,44 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
DrawNew(design);
}
private void DrawMultiButtons(Design design)
{
var any = design.Materials.Count > 0;
var disabled = !_config.DeleteDesignModifier.IsActive();
var size = new Vector2(200 * ImUtf8.GlobalScale, 0);
if (ImUtf8.ButtonEx("Enable All Advanced Dyes"u8,
any
? "Enable the application of all contained advanced dyes without deleting them."u8
: "This design does not contain any advanced dyes."u8, size,
!any || disabled))
_designManager.ChangeApplyMulti(design, null, null, null, null, null, null, true, null);
;
if (disabled && any)
ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to enable.");
ImGui.SameLine();
if (ImUtf8.ButtonEx("Disable All Advanced Dyes"u8,
any
? "Disable the application of all contained advanced dyes without deleting them."u8
: "This design does not contain any advanced dyes."u8, size,
!any || disabled))
_designManager.ChangeApplyMulti(design, null, null, null, null, null, null, false, null);
if (disabled && any)
ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to disable.");
if (ImUtf8.ButtonEx("Delete All Advanced Dyes"u8, any ? ""u8 : "This design does not contain any advanced dyes."u8, size,
!any || disabled))
while (design.Materials.Count > 0)
_designManager.ChangeMaterialValue(design, MaterialValueIndex.FromKey(design.Materials[0].Item1), null);
if (disabled && any)
ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to delete.");
}
private void DrawName(MaterialValueIndex index)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale).Push(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
using var color = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.Text));
ImGuiUtil.DrawTextButton(index.ToString(), new Vector2((GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale + _spacing, 0), 0);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f));
ImUtf8.TextFramed(index.ToString(), 0, new Vector2((GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale + _spacing, 0),
borderColor: ImGui.GetColorU32(ImGuiCol.Text));
}
private void DrawSingleRow(Design design)
@ -54,16 +91,17 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
{
using var id = ImRaii.PushId(i);
var (idx, value) = design.Materials[i];
var key = MaterialValueIndex.FromKey(idx);
var key = MaterialValueIndex.FromKey(idx);
DrawName(key);
ImGui.SameLine(0, _spacing);
DeleteButton(design, key, ref i);
DeleteButton(design, key, ref i);
ImGui.SameLine(0, _spacing);
CopyButton(value.Value);
ImGui.SameLine(0, _spacing);
PasteButton(design, key);
ImGui.SameLine(0, _spacing);
using var disabled = ImRaii.Disabled(design.WriteProtected());
EnabledToggle(design, key, value.Enabled);
ImGui.SameLine(0, _spacing);
DrawRow(design, key, value.Value, value.Revert);
@ -89,7 +127,7 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
PasteButton(design, key);
ImGui.SameLine(0, _spacing);
EnabledToggle(design, key, value.Enabled);
DrawRow(design, key, value.Value, value.Revert);
ImGui.SameLine(0, _spacing);
@ -101,9 +139,9 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
private void DeleteButton(Design design, MaterialValueIndex index, ref int idx)
{
var deleteEnabled = _config.DeleteDesignModifier.IsActive();
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize,
$"Delete this color row.{(deleteEnabled ? string.Empty : $"\nHold {_config.DeleteDesignModifier} to delete.")}",
!deleteEnabled, true))
if (!ImUtf8.IconButton(FontAwesomeIcon.Trash,
$"Delete this color row.{(deleteEnabled ? string.Empty : $"\nHold {_config.DeleteDesignModifier} to delete.")}", disabled:
!deleteEnabled || design.WriteProtected()))
return;
_designManager.ChangeMaterialValue(design, index, null);
@ -112,76 +150,106 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
private void CopyButton(in ColorRow row)
{
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), _buttonSize, "Export this row to your clipboard.",
false,
true))
if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8))
ColorRowClipboard.Row = row;
}
private void PasteButton(Design design, MaterialValueIndex index)
{
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), _buttonSize,
"Import an exported row from your clipboard onto this row.", !ColorRowClipboard.IsSet, true))
if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8,
disabled: !ColorRowClipboard.IsSet || design.WriteProtected()))
_designManager.ChangeMaterialValue(design, index, ColorRowClipboard.Row);
}
private void EnabledToggle(Design design, MaterialValueIndex index, bool enabled)
{
if (ImGui.Checkbox("Enabled", ref enabled))
if (ImUtf8.Checkbox("Enabled"u8, ref enabled))
_designManager.ChangeApplyMaterialValue(design, index, enabled);
}
private void RevertToggle(Design design, MaterialValueIndex index, bool revert)
{
if (ImGui.Checkbox("Revert", ref revert))
if (ImUtf8.Checkbox("Revert"u8, ref revert))
_designManager.ChangeMaterialRevert(design, index, revert);
ImGuiUtil.HoverTooltip(
"If this is checked, Glamourer will try to revert the advanced dye row to its game state instead of applying a specific row.");
ImUtf8.HoverTooltip(
"If this is checked, Glamourer will try to revert the advanced dye row to its game state instead of applying a specific row."u8);
}
public sealed class MaterialSlotCombo;
private void DrawSlotCombo()
{
var width = ImUtf8.CalcTextSize(EquipSlot.OffHand.ToName()).X + ImGui.GetFrameHeightWithSpacing();
ImGui.SetNextItemWidth(width);
using var combo = ImUtf8.Combo("##slot"u8, _newKey.SlotName());
if (combo)
{
var currentSlot = _newKey.ToEquipSlot();
foreach (var tmpSlot in EquipSlotExtensions.FullSlots)
{
if (ImUtf8.Selectable(tmpSlot.ToName(), tmpSlot == currentSlot) && currentSlot != tmpSlot)
_newKey = MaterialValueIndex.FromSlot(tmpSlot) with
{
MaterialIndex = (byte)_newMaterialIdx,
RowIndex = (byte)_newRowIdx,
};
}
var currentBonus = _newKey.ToBonusSlot();
foreach (var bonusSlot in BonusExtensions.AllFlags)
{
if (ImUtf8.Selectable(bonusSlot.ToName(), bonusSlot == currentBonus) && bonusSlot != currentBonus)
_newKey = MaterialValueIndex.FromSlot(bonusSlot) with
{
MaterialIndex = (byte)_newMaterialIdx,
RowIndex = (byte)_newRowIdx,
};
}
}
ImUtf8.HoverTooltip("Choose a slot for an advanced dye row."u8);
}
public void DrawNew(Design design)
{
if (EquipSlotCombo.Draw("##slot", "Choose a slot for an advanced dye row.", ref _newSlot))
_newKey = MaterialValueIndex.FromSlot(_newSlot) with
{
MaterialIndex = (byte)_newMaterialIdx,
RowIndex = (byte)_newRowIdx,
};
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
DrawSlotCombo();
ImUtf8.SameLineInner();
DrawMaterialIdxDrag();
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImUtf8.SameLineInner();
DrawRowIdxDrag();
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImUtf8.SameLineInner();
var exists = design.GetMaterialDataRef().TryGetValue(_newKey, out _);
if (ImGuiUtil.DrawDisabledButton("Add New Row", Vector2.Zero,
exists ? "The selected advanced dye row already exists." : "Add the selected advanced dye row.", exists, false))
if (ImUtf8.ButtonEx("Add New Row"u8,
exists ? "The selected advanced dye row already exists."u8 : "Add the selected advanced dye row."u8, Vector2.Zero,
exists || design.WriteProtected()))
_designManager.ChangeMaterialValue(design, _newKey, ColorRow.Empty);
}
private void DrawMaterialIdxDrag()
{
_newMaterialIdx += 1;
ImGui.SetNextItemWidth(ImGui.CalcTextSize("Material #000").X);
if (ImGui.DragInt("##Material", ref _newMaterialIdx, 0.01f, 1, MaterialService.MaterialsPerModel, "Material #%i"))
ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("Material AA"u8).X);
var format = $"Material {(char)('A' + _newMaterialIdx)}";
if (ImUtf8.DragScalar("##Material"u8, ref _newMaterialIdx, format, 0, MaterialService.MaterialsPerModel - 1, 0.01f,
ImGuiSliderFlags.NoInput))
{
_newMaterialIdx = Math.Clamp(_newMaterialIdx, 1, MaterialService.MaterialsPerModel);
_newKey = _newKey with { MaterialIndex = (byte)(_newMaterialIdx - 1) };
_newMaterialIdx = Math.Clamp(_newMaterialIdx, 0, MaterialService.MaterialsPerModel - 1);
_newKey = _newKey with { MaterialIndex = (byte)_newMaterialIdx };
}
_newMaterialIdx -= 1;
ImUtf8.HoverTooltip("Drag this to the left or right to change its value."u8);
}
private void DrawRowIdxDrag()
{
_newRowIdx += 1;
ImGui.SetNextItemWidth(ImGui.CalcTextSize("Row #0000").X);
if (ImGui.DragInt("##Row", ref _newRowIdx, 0.01f, 1, LegacyColorTable.NumUsedRows, "Row #%i"))
ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("Row 0000"u8).X);
var format = $"Row {_newRowIdx / 2 + 1}{(char)(_newRowIdx % 2 + 'A')}";
if (ImUtf8.DragScalar("##Row"u8, ref _newRowIdx, format, 0, ColorTable.NumRows - 1, 0.01f, ImGuiSliderFlags.NoInput))
{
_newRowIdx = Math.Clamp(_newRowIdx, 1, LegacyColorTable.NumUsedRows);
_newKey = _newKey with { RowIndex = (byte)(_newRowIdx - 1) };
_newRowIdx = Math.Clamp(_newRowIdx, 0, ColorTable.NumRows - 1);
_newKey = _newKey with { RowIndex = (byte)_newRowIdx };
}
_newRowIdx -= 1;
ImUtf8.HoverTooltip("Drag this to the left or right to change its value."u8);
}
private void DrawRow(Design design, MaterialValueIndex index, in ColorRow row, bool disabled)
@ -189,18 +257,18 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
var tmp = row;
using var _ = ImRaii.Disabled(disabled);
var applied = ImGuiUtil.ColorPicker("##diffuse", "Change the diffuse value for this row.", row.Diffuse, v => tmp.Diffuse = v, "D");
ImGui.SameLine(0, _spacing);
ImUtf8.SameLineInner();
applied |= ImGuiUtil.ColorPicker("##specular", "Change the specular value for this row.", row.Specular, v => tmp.Specular = v, "S");
ImGui.SameLine(0, _spacing);
ImUtf8.SameLineInner();
applied |= ImGuiUtil.ColorPicker("##emissive", "Change the emissive value for this row.", row.Emissive, v => tmp.Emissive = v, "E");
ImGui.SameLine(0, _spacing);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(GlossWidth * ImGuiHelpers.GlobalScale);
applied |= ImGui.DragFloat("##Gloss", ref tmp.GlossStrength, 0.01f, 0.001f, float.MaxValue, "%.3f G");
ImGuiUtil.HoverTooltip("Change the gloss strength for this row.");
ImGui.SameLine(0, _spacing);
applied |= AdvancedDyePopup.DragGloss(ref tmp.GlossStrength);
ImUtf8.HoverTooltip("Change the gloss strength for this row."u8);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(SpecularStrengthWidth * ImGuiHelpers.GlobalScale);
applied |= ImGui.DragFloat("##Specular Strength", ref tmp.SpecularStrength, 0.01f, float.MinValue, float.MaxValue, "%.3f SS");
ImGuiUtil.HoverTooltip("Change the specular strength for this row.");
applied |= AdvancedDyePopup.DragSpecularStrength(ref tmp.SpecularStrength);
ImUtf8.HoverTooltip("Change the specular strength for this row."u8);
if (applied)
_designManager.ChangeMaterialValue(design, index, tmp);
}

View file

@ -1,37 +1,40 @@
using Dalamud;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using Penumbra.Api.Enums;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui;
public sealed class PenumbraChangedItemTooltip : IDisposable
{
private readonly PenumbraService _penumbra;
private readonly StateManager _stateManager;
private readonly ItemManager _items;
private readonly ObjectManager _objects;
private readonly CustomizeService _customize;
private readonly GPoseService _gpose;
private readonly PenumbraService _penumbra;
private readonly StateManager _stateManager;
private readonly ItemManager _items;
private readonly ActorObjectManager _objects;
private readonly CustomizeService _customize;
private readonly GPoseService _gpose;
private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2];
private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2 + BonusExtensions.AllFlags.Count];
public IEnumerable<KeyValuePair<EquipSlot, EquipItem>> LastItems
=> EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand).Zip(_lastItems)
.Select(p => new KeyValuePair<EquipSlot, EquipItem>(p.First, p.Second));
public IEnumerable<KeyValuePair<object, EquipItem>> LastItems
=> EquipSlotExtensions.EqdpSlots.Cast<object>().Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)
.Concat(BonusExtensions.AllFlags.Cast<object>()).Zip(_lastItems)
.Select(p => new KeyValuePair<object, EquipItem>(p.First, p.Second));
public DateTime LastTooltip { get; private set; } = DateTime.MinValue;
public DateTime LastClick { get; private set; } = DateTime.MinValue;
public ChangedItemType LastType { get; private set; } = ChangedItemType.None;
public uint LastId { get; private set; }
public DateTime LastTooltip { get; private set; } = DateTime.MinValue;
public DateTime LastClick { get; private set; } = DateTime.MinValue;
public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ObjectManager objects,
public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ActorObjectManager objects,
CustomizeService customize, GPoseService gpose)
{
_penumbra = penumbra;
@ -70,6 +73,21 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
if (!Player())
return;
var bonusSlot = item.Type.ToBonus();
if (bonusSlot is not BonusItemFlag.Unknown)
{
// + 2 due to weapons.
var glasses = _lastItems[bonusSlot.ToSlot() + 2];
using (_ = !openTooltip ? null : ImRaii.Tooltip())
{
ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor.");
if (glasses.Valid)
ImGui.TextUnformatted($"{prefix}Control + Right-Click to re-apply {glasses.Name} to current actor.");
}
return;
}
var slot = item.Type.ToSlot();
var last = _lastItems[slot.ToIndex()];
switch (slot)
@ -107,6 +125,27 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
public void ApplyItem(ActorState state, EquipItem item)
{
var bonusSlot = item.Type.ToBonus();
if (bonusSlot is not BonusItemFlag.Unknown)
{
// + 2 due to weapons.
var glasses = _lastItems[bonusSlot.ToSlot() + 2];
if (ImGui.GetIO().KeyCtrl && glasses.Valid)
{
Glamourer.Log.Debug($"Re-Applying {glasses.Name} to {bonusSlot.ToName()}.");
SetLastItem(bonusSlot, default, state);
_stateManager.ChangeBonusItem(state, bonusSlot, glasses, ApplySettings.Manual);
}
else
{
Glamourer.Log.Debug($"Applying {item.Name} to {bonusSlot.ToName()}.");
SetLastItem(bonusSlot, item, state);
_stateManager.ChangeBonusItem(state, bonusSlot, item, ApplySettings.Manual);
}
return;
}
var slot = item.Type.ToSlot();
var last = _lastItems[slot.ToIndex()];
switch (slot)
@ -160,6 +199,8 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
private void OnPenumbraTooltip(ChangedItemType type, uint id)
{
LastType = type;
LastId = id;
LastTooltip = DateTime.UtcNow;
if (!Player())
return;
@ -168,13 +209,24 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
{
case ChangedItemType.ItemOffhand:
case ChangedItemType.Item:
{
if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item))
return;
CreateTooltip(item, "[Glamourer] ", false);
return;
}
case ChangedItemType.CustomArmor:
{
var (model, variant, slot) = IdentifiedItem.Split(id);
var item = _items.Identify(slot.ToSlot(), model, variant);
if (item.Valid)
CreateTooltip(item, "[Glamourer] ", false);
return;
}
case ChangedItemType.Customization:
var (race, gender, index, value) = ChangedItemExtensions.Split(id);
{
var (race, gender, index, value) = IdentifiedCustomization.Split(id);
if (!_objects.Player.Model.IsHuman)
return;
@ -183,6 +235,7 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
ImGui.TextUnformatted("[Glamourer] Right-Click to apply to current actor.");
return;
}
}
}
@ -192,7 +245,7 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
return true;
var main = _objects.Player.GetMainhand();
var mainItem = _items.Identify(slot, main.Skeleton, main.Weapon, main.Variant);
var mainItem = _items.Identify(EquipSlot.MainHand, main.Skeleton, main.Weapon, main.Variant);
if (slot == EquipSlot.MainHand)
return item.Type == mainItem.Type;
@ -212,17 +265,29 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
{
case ChangedItemType.Item:
case ChangedItemType.ItemOffhand:
{
if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item))
return;
ApplyItem(state, item);
return;
}
case ChangedItemType.CustomArmor:
{
var (model, variant, slot) = IdentifiedItem.Split(id);
var item = _items.Identify(slot.ToSlot(), model, variant);
if (item.Valid)
ApplyItem(state, item);
return;
}
case ChangedItemType.Customization:
var (race, gender, index, value) = ChangedItemExtensions.Split(id);
{
var (race, gender, index, value) = IdentifiedCustomization.Split(id);
var customize = state.ModelData.Customize;
if (CheckGenderRace(customize, race, gender) && VerifyValue(customize, index, value))
_stateManager.ChangeCustomize(state, index, value, ApplySettings.Manual);
return;
}
}
}
@ -236,8 +301,23 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
else
{
var oldItem = state.ModelData.Item(slot);
if (oldItem.ItemId != item.ItemId)
_lastItems[slot.ToIndex()] = oldItem;
if (oldItem.Id != item.Id)
last = oldItem;
}
}
private void SetLastItem(BonusItemFlag slot, EquipItem item, ActorState state)
{
ref var last = ref _lastItems[slot.ToSlot() + 2];
if (!item.Valid)
{
last = default;
}
else
{
var oldItem = state.ModelData.BonusItem(slot);
if (oldItem.Id != item.Id)
last = oldItem;
}
}

View file

@ -1,25 +1,26 @@
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Gui.Materials;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.State;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.HelperObjects;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using ObjectManager = Glamourer.Interop.ObjectManager;
namespace Glamourer.Gui.Tabs.ActorTab;
@ -32,15 +33,16 @@ public class ActorPanel
private readonly AutoDesignApplier _autoDesignApplier;
private readonly Configuration _config;
private readonly DesignConverter _converter;
private readonly ObjectManager _objects;
private readonly ActorObjectManager _objects;
private readonly DesignManager _designManager;
private readonly ImportService _importService;
private readonly ICondition _conditions;
private readonly DictModelChara _modelChara;
private readonly CustomizeParameterDrawer _parameterDrawer;
private readonly AdvancedDyePopup _advancedDyes;
private readonly HeaderDrawer.Button[] _leftButtons;
private readonly HeaderDrawer.Button[] _rightButtons;
private readonly EditorHistory _editorHistory;
private readonly HeaderDrawer.Button[] _leftButtons;
private readonly HeaderDrawer.Button[] _rightButtons;
public ActorPanel(ActorSelector selector,
StateManager stateManager,
@ -49,13 +51,14 @@ public class ActorPanel
AutoDesignApplier autoDesignApplier,
Configuration config,
DesignConverter converter,
ObjectManager objects,
ActorObjectManager objects,
DesignManager designManager,
ImportService importService,
ICondition conditions,
DictModelChara modelChara,
CustomizeParameterDrawer parameterDrawer,
AdvancedDyePopup advancedDyes)
AdvancedDyePopup advancedDyes,
EditorHistory editorHistory)
{
_selector = selector;
_stateManager = stateManager;
@ -71,16 +74,18 @@ public class ActorPanel
_modelChara = modelChara;
_parameterDrawer = parameterDrawer;
_advancedDyes = advancedDyes;
_editorHistory = editorHistory;
_leftButtons =
[
new SetFromClipboardButton(this),
new ExportToClipboardButton(this),
new SaveAsDesignButton(this),
new UndoButton(this),
];
_rightButtons =
[
new LockedButton(this),
new HeaderDrawer.IncognitoButton(_config.Ephemeral),
new HeaderDrawer.IncognitoButton(_config),
];
}
@ -99,7 +104,7 @@ public class ActorPanel
{
using var group = ImRaii.Group();
(_identifier, _data) = _selector.Selection;
_lockedRedraw = _identifier.Type is IdentifierType.Special
_lockedRedraw = _identifier.Type is IdentifierType.Special || _objects.IsInLobby
|| _conditions[ConditionFlag.OccupiedInCutSceneEvent];
(_actorName, _actor) = GetHeaderName();
DrawHeader();
@ -147,10 +152,13 @@ public class ActorPanel
private unsafe void DrawPanel()
{
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state))
using var table = ImUtf8.Table("##Panel", 1, ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail());
if (!table || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state))
return;
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableNextColumn();
ImGui.Dummy(Vector2.Zero);
var transformationId = _actor.IsCharacter ? _actor.AsCharacter->CharacterData.TransformationId : 0;
if (transformationId != 0)
ImGuiUtil.DrawTextButton($"Currently transformed to Transformation {transformationId}.",
@ -161,13 +169,15 @@ public class ActorPanel
DrawApplyToTarget();
RevertButtons();
ImGui.TableNextColumn();
using var disabled = ImRaii.Disabled(transformationId != 0);
if (_state.ModelData.IsHuman)
DrawHumanPanel();
else
DrawMonsterPanel();
_advancedDyes.Draw(_data.Objects.Last(), _state);
if (_data.Objects.Count > 0)
_advancedDyes.Draw(_data.Objects.Last(), _state);
}
private void DrawHumanPanel()
@ -175,14 +185,19 @@ public class ActorPanel
DrawCustomizationsHeader();
DrawEquipmentHeader();
DrawParameterHeader();
DrawDebugData();
}
private void DrawCustomizationsHeader()
{
if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization))
return;
var header = _state!.ModelData.ModelId == 0
? "Customization"
: $"Customization (Model Id #{_state.ModelData.ModelId})###Customization";
using var h = ImRaii.CollapsingHeader(header);
var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization);
using var h = ImUtf8.CollapsingHeaderId(header, expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
if (!h)
return;
@ -195,7 +210,7 @@ public class ActorPanel
private void DrawEquipmentHeader()
{
using var h = ImRaii.CollapsingHeader("Equipment");
using var h = DesignPanelFlag.Equipment.Header(_config);
if (!h)
return;
@ -207,30 +222,68 @@ public class ActorPanel
var data = EquipDrawData.FromState(_stateManager, _state!, slot);
_equipmentDrawer.DrawEquip(data);
if (usedAllStain)
_stateManager.ChangeStain(_state, slot, newAllStain, ApplySettings.Manual);
_stateManager.ChangeStains(_state, slot, newAllStain, ApplySettings.Manual);
}
var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand);
var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand);
_equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose());
foreach (var slot in BonusExtensions.AllFlags)
{
var data = BonusDrawData.FromState(_stateManager, _state!, slot);
_equipmentDrawer.DrawBonusItem(data);
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawEquipmentMetaToggles();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
_equipmentDrawer.DrawDragDropTooltip();
}
private void DrawParameterHeader()
{
if (!_config.UseAdvancedParameters)
return;
using var h = ImRaii.CollapsingHeader("Advanced Customizations");
using var h = DesignPanelFlag.AdvancedCustomizations.Header(_config);
if (!h)
return;
_parameterDrawer.Draw(_stateManager, _state!);
}
private unsafe void DrawDebugData()
{
if (!_config.DebugMode)
return;
using var h = DesignPanelFlag.DebugData.Header(_config);
if (!h)
return;
using var t = ImUtf8.Table("table"u8, 2, ImGuiTableFlags.SizingFixedFit);
if (!t)
return;
ImUtf8.DrawTableColumn("Object Index"u8);
DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->ObjectIndex))}");
ImUtf8.DrawTableColumn("Name ID"u8);
DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->GetNameId()))}");
ImUtf8.DrawTableColumn("Base ID"u8);
DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->BaseId))}");
ImUtf8.DrawTableColumn("Entity ID"u8);
DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->EntityId))}");
ImUtf8.DrawTableColumn("Owner ID"u8);
DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->OwnerId))}");
ImUtf8.DrawTableColumn("Game Object ID"u8);
DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->GetGameObjectId().ObjectId))}");
static void DrawCopyColumn(ref Utf8StringHandler<TextStringHandlerBuffer> text)
{
ImUtf8.DrawTableColumn(ref text);
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
ImUtf8.SetClipboardText(TextStringHandlerBuffer.Span);
}
}
private void DrawEquipmentMetaToggles()
{
using (_ = ImRaii.Group())
@ -252,6 +305,12 @@ public class ActorPanel
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.WeaponState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!));
}
ImGui.SameLine();
using (_ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.EarState, _stateManager, _state!));
}
}
private void DrawMonsterPanel()
@ -333,7 +392,7 @@ public class ActorPanel
{
if (ImGuiUtil.DrawDisabledButton("Revert to Game", Vector2.Zero, "Revert the character to its actual state in the game.",
_state!.IsLocked))
_stateManager.ResetState(_state!, StateSource.Manual);
_stateManager.ResetState(_state!, StateSource.Manual, isFinal: true);
ImGui.SameLine();
@ -341,8 +400,8 @@ public class ActorPanel
"Reapply the current automation state for the character on top of its current state..",
!_config.EnableAutoDesigns || _state!.IsLocked))
{
_autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, out var forcedRedraw);
_stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual);
_autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, false, out var forcedRedraw);
_stateManager.ReapplyAutomationState(_actor, forcedRedraw, false, StateSource.Manual);
}
ImGui.SameLine();
@ -350,15 +409,15 @@ public class ActorPanel
"Try to revert the character to the state it would have using automated designs.",
!_config.EnableAutoDesigns || _state!.IsLocked))
{
_autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, out var forcedRedraw);
_stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual);
_autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, false, out var forcedRedraw);
_stateManager.ReapplyAutomationState(_actor, forcedRedraw, true, StateSource.Manual);
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Reapply", Vector2.Zero,
"Try to reapply the configured state if something went wrong. Should generally not be necessary.",
_state!.IsLocked))
_stateManager.ReapplyState(_actor, false, StateSource.Manual);
_stateManager.ReapplyState(_actor, false, StateSource.Manual, true);
}
private void DrawApplyToSelf()
@ -371,7 +430,7 @@ public class ActorPanel
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)),
ApplySettings.Manual);
ApplySettings.Manual with { IsFinal = true });
}
private void DrawApplyToTarget()
@ -388,7 +447,7 @@ public class ActorPanel
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)),
ApplySettings.Manual);
ApplySettings.Manual with { IsFinal = true });
}
@ -415,7 +474,7 @@ public class ActorPanel
var text = ImGui.GetClipboardText();
var design = panel._converter.FromBase64(text, applyCustomize, applyGear, out _)
?? throw new Exception("The clipboard did not contain valid data.");
panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks);
panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks with { IsFinal = true });
}
catch (Exception ex)
{
@ -470,6 +529,24 @@ public class ActorPanel
}
}
private sealed class UndoButton(ActorPanel panel) : HeaderDrawer.Button
{
protected override string Description
=> "Undo the last change.";
protected override FontAwesomeIcon Icon
=> FontAwesomeIcon.Undo;
public override bool Visible
=> panel._state != null;
protected override bool Disabled
=> (panel._state?.IsLocked ?? true) || !panel._editorHistory.CanUndo(panel._state);
protected override void OnClick()
=> panel._editorHistory.Undo(panel._state!);
}
private sealed class LockedButton(ActorPanel panel) : HeaderDrawer.Button
{
protected override string Description

View file

@ -1,15 +1,17 @@
using Dalamud.Interface;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.ActorTab;
public class ActorSelector(ObjectManager objects, ActorManager actors, EphemeralConfig config)
public class ActorSelector(ActorObjectManager objects, ActorManager actors, EphemeralConfig config)
{
private ActorIdentifier _identifier = ActorIdentifier.Invalid;
@ -25,6 +27,7 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral
private LowerString _actorFilter = LowerString.Empty;
private Vector2 _defaultItemSpacing;
private WorldId _world;
private float _width;
public (ActorIdentifier Identifier, ActorData Data) Selection
@ -36,12 +39,43 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral
public void Draw(float width)
{
_width = width;
using var group = ImRaii.Group();
using var group = ImUtf8.Group();
_defaultItemSpacing = ImGui.GetStyle().ItemSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetNextItemWidth(_width);
LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64);
if (ImGui.IsItemHovered())
{
using var tt = ImUtf8.Tooltip();
ImUtf8.Text("Filter for names containing the input."u8);
ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeight() / 2));
ImUtf8.Text("Special filters are:"u8);
var color = ColorId.HeaderButtons.Value();
ImUtf8.Text("<p>"u8, color);
ImGui.SameLine(0, 0);
ImUtf8.Text(": show only player characters."u8);
ImUtf8.Text("<o>"u8, color);
ImGui.SameLine(0, 0);
ImUtf8.Text(": show only owned game objects."u8);
ImUtf8.Text("<n>"u8, color);
ImGui.SameLine(0, 0);
ImUtf8.Text(": show only NPCs."u8);
ImUtf8.Text("<r>"u8, color);
ImGui.SameLine(0, 0);
ImUtf8.Text(": show only retainers."u8);
ImUtf8.Text("<s>"u8, color);
ImGui.SameLine(0, 0);
ImUtf8.Text(": show only special screen characters."u8);
ImUtf8.Text("<w>"u8, color);
ImGui.SameLine(0, 0);
ImUtf8.Text(": show only players from your world."u8);
}
DrawSelector();
DrawSelectionButtons();
@ -49,24 +83,35 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral
private void DrawSelector()
{
using var child = ImRaii.Child("##Selector", new Vector2(_width, -ImGui.GetFrameHeight()), true);
using var child = ImUtf8.Child("##Selector"u8, new Vector2(_width, -ImGui.GetFrameHeight()), true);
if (!child)
return;
objects.Update();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(objects.Identifiers, skips, CheckFilter, DrawSelectable);
_world = new WorldId(objects.Player.Valid ? objects.Player.HomeWorld : (ushort)0);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(objects.Where(p => p.Value.Objects.Any(a => a.Model)), skips, CheckFilter,
DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
}
private bool CheckFilter(KeyValuePair<ActorIdentifier, ActorData> pair)
=> _actorFilter.IsEmpty || pair.Value.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase);
=> _actorFilter.Lower switch
{
"" => true,
"<p>" => pair.Key.Type is IdentifierType.Player,
"<o>" => pair.Key.Type is IdentifierType.Owned,
"<n>" => pair.Key.Type is IdentifierType.Npc,
"<r>" => pair.Key.Type is IdentifierType.Retainer,
"<s>" => pair.Key.Type is IdentifierType.Special,
"<w>" => pair.Key.Type is IdentifierType.Player && pair.Key.HomeWorld == _world,
_ => _actorFilter.IsContained(pair.Value.Label),
};
private void DrawSelectable(KeyValuePair<ActorIdentifier, ActorData> pair)
{
var equals = pair.Key.Equals(_identifier);
if (ImGui.Selectable(IncognitoMode ? pair.Key.Incognito(pair.Value.Label) : pair.Value.Label, equals) && !equals)
if (ImUtf8.Selectable(IncognitoMode ? pair.Key.Incognito(pair.Value.Label) : pair.Value.Label, equals) && !equals)
_identifier = pair.Key.CreatePermanent();
}
@ -76,15 +121,14 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral
.Push(ImGuiStyleVar.FrameRounding, 0);
var buttonWidth = new Vector2(_width / 2, 0);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth
, "Select the local player character.", !objects.Player, true))
if (ImUtf8.IconButton(FontAwesomeIcon.UserCircle, "Select the local player character."u8, buttonWidth, !objects.Player))
_identifier = objects.Player.GetIdentifier(actors);
ImGui.SameLine();
var (id, data) = objects.TargetData;
var tt = data.Valid ? $"Select the current target {id} in the list." :
id.IsValid ? $"The target {id} is not in the list." : "No target selected.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, tt, objects.IsInGPose || !data.Valid, true))
if (ImUtf8.IconButton(FontAwesomeIcon.HandPointer, tt, buttonWidth, objects.IsInGPose || !data.Valid))
_identifier = id;
}
}

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