Compare commits

...

403 commits

Author SHA1 Message Date
Karou
ccb5b01290 Api version bump and remove redundant framework thread call
Some checks failed
.NET Build / build (push) Has been cancelled
2025-12-05 13:39:19 +01:00
Actions User
5dd74297c6 [CI] Updating repo.json for 1.5.1.8
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-28 22:10:17 +00:00
Karou
ce54aa5d25 Added IPC call to allow for redrawing only members of specified collections
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-03 15:15:40 +01:00
Actions User
c4b6e4e00b [CI] Updating repo.json for testing_1.5.1.7
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-23 21:50:20 +00:00
Ottermandias
912c183fc6 Improve file watcher. 2025-10-23 23:45:20 +02:00
Ottermandias
5bf901d0c4 Update actorobjectmanager when setting cutscene index.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-23 17:30:29 +02:00
Ottermandias
cbedc878b9 Slight cleanup and autoformat. 2025-10-22 21:56:16 +02:00
Ottermandias
c8cf560fc1 Merge branch 'refs/heads/StoiaCode/fileWatcher' 2025-10-22 21:48:42 +02:00
Stoia
f05cb52da2 Add Option to notify instead of auto install.
And General Fixes
2025-10-22 18:20:44 +02:00
Ottermandias
7ed81a9823 Update OtterGui.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-22 17:53:02 +02:00
Stoia
60aa23efcd
Merge branch 'xivdev:master' into fileWatcher 2025-10-22 14:28:08 +02:00
Ottermandias
ebbe957c95 Remove login screen log spam.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-11 20:13:51 +02:00
Actions User
300e0e6d84 [CI] Updating repo.json for 1.5.1.6
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-07 10:45:04 +00:00
Ottermandias
049baa4fe4 Again. 2025-10-07 12:42:54 +02:00
Ottermandias
0881dfde8a Update signatures. 2025-10-07 12:27:35 +02:00
Actions User
23c0506cb8 [CI] Updating repo.json for testing_1.5.1.5
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-28 10:43:01 +00:00
Ottermandias
699745413e Make priority an int. 2025-09-28 12:40:52 +02:00
Actions User
eb53f04c6b [CI] Updating repo.json for testing_1.5.1.4
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 12:03:35 +00:00
Ottermandias
c6b596169c Add default constructor. 2025-09-27 14:01:21 +02:00
Actions User
a0c3e820b0 [CI] Updating repo.json for testing_1.5.1.3
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 11:02:39 +00:00
Ottermandias
a59689ebfe CS API update and add http API routes. 2025-09-27 13:00:18 +02:00
Exter-N
e9f67a009b Lift "shaders known" restriction for saving materials
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-19 11:18:39 +02:00
Ottermandias
97c8d82b33 Prevent default-named collection from being renamed and always put it at the top of the selector.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-07 10:45:28 +02:00
Stoia
c3b00ff426 Integrate FileWatcher
HEAVY WIP
2025-09-06 14:22:18 +02:00
Actions User
6348c4a639 [CI] Updating repo.json for 1.5.1.2
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-02 14:25:55 +00:00
Ottermandias
5a6e06df3b git is stupid 2025-09-02 16:22:02 +02:00
Ottermandias
f5f6dd3246 Handle some TODOs. 2025-09-02 16:12:01 +02:00
Ottermandias
4e788f7c2b Update sig. 2025-09-02 11:51:59 +02:00
Ottermandias
ad1659caf6 Update libraries. 2025-09-02 11:29:58 +02:00
Ottermandias
18a6ce2a5f Merge branch 'refs/heads/Exter-N/cldapi'
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-01 15:59:26 +02:00
Ottermandias
e68e821b2a Merge branch 'master' into Exter-N/cldapi 2025-09-01 15:58:22 +02:00
Ottermandias
96764b34ca Merge branch 'refs/heads/Exter-N/restree-stuff' 2025-09-01 15:57:06 +02:00
Exter-N
2cf60b78cd Reject and warn about cloud-synced base directories 2025-08-31 06:42:45 +02:00
Exter-N
d59be1e660 Refine IsCloudSynced 2025-08-31 05:25:37 +02:00
Exter-N
5503bb32e0 CloudApi testing in Debug tab 2025-08-31 04:13:56 +02:00
Exter-N
f3ec4b2e08 Only display the file name and last dir for externals 2025-08-30 19:19:07 +02:00
Exter-N
b3379a9710 Stop redacting external paths 2025-08-30 16:55:20 +02:00
Exter-N
8c25ef4b47 Make the save button ResourceTreeViewer baseline 2025-08-30 16:53:12 +02:00
Ottermandias
912020cc3f Update for staging and wrong tooltip.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-29 16:36:42 +02:00
Ottermandias
be8987a451 Merge branch 'master' of github.com:xivDev/Penumbra
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-28 18:52:29 +02:00
Ottermandias
f7cf5503bb Fix deleting PCP collections. 2025-08-28 18:52:06 +02:00
Ottermandias
a04a5a071c Add warning in file redirections if extension doesn't match. 2025-08-28 18:51:57 +02:00
Actions User
71e24c13c7 [CI] Updating repo.json for 1.5.1.0
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-25 08:39:42 +00:00
Ottermandias
c0120f81af 1.5.1.0 2025-08-25 10:37:38 +02:00
Ottermandias
da47c19aeb Woops, increment version. 2025-08-25 10:25:05 +02:00
Actions User
e16800f216 [CI] Updating repo.json for testing_1.5.0.10 2025-08-25 08:16:04 +00:00
Ottermandias
79a4fc5904 Fix wrong logging. 2025-08-25 10:13:48 +02:00
Ottermandias
bf90725dd2 Fix resolvecontext issue. 2025-08-25 10:13:39 +02:00
Ottermandias
a14347f73a Update temporary collection creation. 2025-08-25 10:13:31 +02:00
Actions User
1e07e43498 [CI] Updating repo.json for testing_1.5.0.9
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-24 13:51:43 +00:00
Ottermandias
f51f8a7bf8 Try to filter meta entries for relevance. 2025-08-24 15:24:57 +02:00
Exter-N
1fca78fa71 Add Kdb files to ResourceTree 2025-08-24 14:09:02 +02:00
Exter-N
c8b6325a87 Add game integrity message to On-Screen 2025-08-24 14:06:39 +02:00
Ottermandias
6079103505 Add collection PCP settings.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-23 14:46:27 +02:00
Actions User
d302a17f1f [CI] Updating repo.json for testing_1.5.0.8
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-22 18:33:43 +00:00
Ottermandias
0d64384059 Add cleanup buttons to PCP, add option to turn off PCP IPC. 2025-08-22 20:31:40 +02:00
Ottermandias
10894d451a Add Pcp Events. 2025-08-22 18:08:22 +02:00
Actions User
fb34238530 [CI] Updating repo.json for testing_1.5.0.7 2025-08-22 13:51:50 +00:00
Ottermandias
8043e6fb6b Add option to disable PCP. 2025-08-22 15:49:15 +02:00
Ottermandias
e3b7f72893 Add initial PCP. 2025-08-22 15:44:33 +02:00
Ottermandias
b7f326e29c Fix bug with collection setting and empty collection. 2025-08-22 15:43:55 +02:00
Ottermandias
dad01e1af8 Update GameData. 2025-08-20 15:24:00 +02:00
Ottermandias
10b71930a1 Merge branch 'refs/heads/Exter-N/stockings-skin-slot' 2025-08-18 15:41:22 +02:00
Ottermandias
23257f94a4 Some cleanup and add option to disable skin material attribute scanning. 2025-08-18 15:41:10 +02:00
Ottermandias
83a36ed4cb Merge branch 'master' into Exter-N/stockings-skin-slot 2025-08-18 15:31:52 +02:00
Ottermandias
8304579d29 Add predefined tags to the multi mod selector. 2025-08-17 13:54:36 +02:00
Actions User
24cbc6c5e1 [CI] Updating repo.json for 1.5.0.6 2025-08-17 08:46:26 +00:00
Exter-N
41edc23820 Allow changing the skin mtrl suffix 2025-08-17 03:11:11 +02:00
Exter-N
aa920b5e9b Fix ImGui texture usage issue 2025-08-17 01:41:49 +02:00
Ottermandias
87ace28bcf Update OtterGui. 2025-08-16 11:56:24 +02:00
Ottermandias
5917f5fad1 Small fixes. 2025-08-13 17:42:45 +02:00
Actions User
f69c264317 [CI] Updating repo.json for 1.5.0.5 2025-08-13 14:53:11 +00:00
Ottermandias
a7246b9d98 Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it. 2025-08-13 16:50:26 +02:00
Actions User
9aff388e21 [CI] Updating repo.json for 1.5.0.4 2025-08-12 12:53:33 +00:00
Ottermandias
091aff1b8a Merge branch 'master' of github.com:xivDev/Penumbra 2025-08-12 14:47:50 +02:00
Ottermandias
9f8185f67b Add new parameter to LoadWeapon hook. 2025-08-12 14:47:35 +02:00
Actions User
b112d75a27 [CI] Updating repo.json for 1.5.0.3 2025-08-12 10:31:13 +00:00
Ottermandias
7af81a6c18 Fix issue with removing default metadata. 2025-08-12 12:29:09 +02:00
Ottermandias
12a218bb2b Protect against empty requested paths. 2025-08-12 12:28:56 +02:00
Ottermandias
f6bac93db7 Update ChangedEquipData. 2025-08-11 19:58:24 +02:00
Actions User
155d3d49aa [CI] Updating repo.json for 1.5.0.2 2025-08-09 16:40:42 +00:00
Exter-N
9aae2210a2 Fix nullptr crashes 2025-08-09 14:57:15 +02:00
Actions User
3785a629ce [CI] Updating repo.json for 1.5.0.1 2025-08-09 11:03:24 +00:00
Ottermandias
02af52671f Need staging again ... 2025-08-09 13:00:40 +02:00
Ottermandias
391c9d727e Fix shifted timeline vfunc offset. 2025-08-09 12:51:39 +02:00
Ottermandias
ff2b2be953 Fix popups not working early. 2025-08-09 12:11:29 +02:00
Ottermandias
6242b30f93 Fix resizable child. 2025-08-09 11:58:35 +02:00
Exter-N
11cd08a9de ClientStructs-ify stuff 2025-08-09 10:29:29 +02:00
Ottermandias
46cfbcb115 Set Repo API level to 13 and remove stg from future releases. 2025-08-08 23:13:23 +02:00
Actions User
66543cc671 [CI] Updating repo.json for 1.5.0.0 2025-08-08 21:12:00 +00:00
Ottermandias
13283c9690 Fix dumb. 2025-08-08 23:08:26 +02:00
Ottermandias
bedfb22466 Use staging for release. 2025-08-08 23:04:50 +02:00
Ottermandias
13df8b2248 Update gamedata. 2025-08-08 23:02:22 +02:00
Ottermandias
93406e4d4e 1.5.0.0 2025-08-08 16:17:59 +02:00
Ridan Vandenbergh
8140d08557 Add vertex material types for usages of 2 colour attributes 2025-08-08 16:15:19 +02:00
Ridan Vandenbergh
2b36f39848 Fix basecolor texture in material export 2025-08-08 16:12:37 +02:00
Ottermandias
a69811800d Update GameData 2025-08-08 15:56:25 +02:00
Ottermandias
3f18ad50de Initial API13 / 7.3 update. 2025-08-08 00:45:24 +02:00
Ridan Vandenbergh
6689e326ee Material tab: disallow "Enable Transparency" for stockings shader 2025-08-02 00:38:24 +02:00
Passive
bdcab22a55 Cleanup methods to extension class 2025-08-02 00:16:55 +02:00
Passive
f5f4fe7259 Invalid tangent fix example 2025-08-02 00:16:55 +02:00
Sebastina
898963fea5 Allow focusing a specified mod via HTTP API under the mods tab. 2025-08-02 00:16:38 +02:00
Ottermandias
8527bfa29c Fix missing updates for OtterGui. 2025-08-02 00:13:35 +02:00
Ottermandias
baca3cdec2 Update Libs. 2025-08-02 00:08:09 +02:00
Ottermandias
dc93eba34c Add initial complex group things. 2025-08-02 00:06:25 +02:00
Ottermandias
012052daa0 Change behavior for directory names. 2025-08-02 00:06:03 +02:00
Ottermandias
a9546e31ee Update packages. 2025-08-02 00:05:27 +02:00
Actions User
a4a6283e7b [CI] Updating repo.json for testing_1.4.0.6 2025-07-14 15:12:06 +00:00
Ottermandias
00c02fd16e Fix tex file migration for small textures. 2025-07-14 17:09:07 +02:00
Ottermandias
140d150bb4 Fix character sound data. 2025-07-14 17:08:46 +02:00
Actions User
49a6d935f3 [CI] Updating repo.json for testing_1.4.0.5 2025-07-05 20:11:28 +00:00
Ottermandias
692beacc2e Merge remote-tracking branch 'Exter-N/human-skin-materials' 2025-07-05 22:04:48 +02:00
Ottermandias
a953febfba Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models. 2025-07-05 22:03:32 +02:00
Ottermandias
c0aa2e36ea Merge branch 'refs/heads/Exter-N/reslogger-tid' 2025-07-05 22:03:14 +02:00
Exter-N
278bf43b29 ClientStructs-ify ResourceTree stuff 2025-07-05 05:20:24 +02:00
Exter-N
a97d9e4953 Add Human skin material handling 2025-07-05 04:37:37 +02:00
Exter-N
30e3cd1f38 Add OS thread ID info to the Resource Logger 2025-07-04 19:41:31 +02:00
Ottermandias
62e9dc164d Add support button. 2025-06-26 14:49:28 +02:00
Actions User
9fc572ba0c [CI] Updating repo.json for testing_1.4.0.4 2025-06-15 21:47:41 +00:00
Ottermandias
3c20b541ce Make mousewheel-scrolling work for setting combos, also filters. 2025-06-15 23:20:13 +02:00
Ottermandias
1961b03d37 Fix issues with shapes and attributes with ID. 2025-06-15 23:18:46 +02:00
Ottermandias
1f4ec984b3 Use improved filesystem. 2025-06-13 17:27:56 +02:00
Ottermandias
4981b0348f BNPCs. 2025-06-13 17:27:56 +02:00
Ottermandias
a8c05fc6ee Make middle-mouse button handle temporary settings. 2025-06-13 17:27:56 +02:00
Actions User
3d05662384 [CI] Updating repo.json for testing_1.4.0.3 2025-06-08 09:38:30 +00:00
Ottermandias
973814b31b Some more BNPCs. 2025-06-08 11:36:32 +02:00
Ottermandias
a16fd85a7e Handle .tex files with broken mip map offsets on import, also remove unnecessary mipmaps (any after reaching minimum size once). 2025-06-08 11:28:12 +02:00
Ottermandias
4c0e6d2a67 Update Mod Merger for other group types. 2025-06-07 22:10:59 +02:00
Ottermandias
535694e9c8 Update some BNPC Names. 2025-06-07 22:10:17 +02:00
Ottermandias
318a41fe52 Add checking for supported features with the currently new supported features 'Atch', 'Shp' and 'Atr'. 2025-06-03 18:39:54 +02:00
Actions User
98203e4e8a [CI] Updating repo.json for testing_1.4.0.2 2025-06-01 11:06:37 +00:00
Ottermandias
6cba63ac98 Make shape names editable in models. 2025-06-01 13:04:26 +02:00
Ottermandias
b48c4f440a Make attributes and shapes completely toggleable. 2025-06-01 13:04:26 +02:00
Actions User
75f4e66dbf [CI] Updating repo.json for 1.4.0.1 2025-05-30 12:38:32 +00:00
Ottermandias
74bd1cf911 Fix checking the flags for all races and genders for specific IDs in shapes/attributes. 2025-05-30 14:36:33 +02:00
Ottermandias
ff2a9f95c4 Fix Atr and Shp not being transmitted via Mare, add improved compression but don't use it yet. 2025-05-30 14:36:07 +02:00
Ottermandias
9921c3332e Merge branch 'master' of github.com:xivDev/Penumbra 2025-05-30 14:35:24 +02:00
Ottermandias
f2927290f5 Fix exceptions when unsubscribing during event invocation. 2025-05-30 14:35:13 +02:00
Actions User
1551d9b6f3 [CI] Updating repo.json for 1.4.0.0 2025-05-28 11:56:49 +00:00
Ottermandias
5e985f4a84 1.4.0.0 2025-05-28 13:54:42 +02:00
Ottermandias
2c115eda94 Slightly improve error message when importing wrongly named atch files. 2025-05-28 13:54:23 +02:00
Ottermandias
ebe45c6a47 Update Lib. 2025-05-27 11:33:10 +02:00
Ottermandias
82fc334be7 Use dynamis for some pointers. 2025-05-23 15:17:19 +02:00
Actions User
cd56163b1b [CI] Updating repo.json for testing_1.3.6.15 2025-05-23 09:32:11 +00:00
Ottermandias
ccc2c1fd4c Fix missing other option notifications for shp/atr. 2025-05-23 11:30:10 +02:00
Ottermandias
08c9124858 Fix issue with shapes/attributes not checking the groups correctly. 2025-05-23 11:29:52 +02:00
Ottermandias
1bdbfe22c1 Update Libraries. 2025-05-23 10:50:04 +02:00
Actions User
9e7c304556 [CI] Updating repo.json for testing_1.3.6.14 2025-05-22 09:16:22 +00:00
Ottermandias
bc4f88aee9 Fix shape/attribute mask stupidity. 2025-05-22 11:14:17 +02:00
Ottermandias
400d7d0bea Slight improvements. 2025-05-22 11:13:58 +02:00
Ottermandias
ac4c75d3c3 Fix not updating meta count correctly. 2025-05-22 11:13:42 +02:00
Ottermandias
507b0a5aee Slight description update. 2025-05-21 18:07:17 +02:00
Actions User
f5db888bbd [CI] Updating repo.json for testing_1.3.6.13 2025-05-21 13:49:29 +00:00
Ottermandias
d7dee39fab Add attribute handling, rework atr and shape caches. 2025-05-21 15:45:05 +02:00
Ottermandias
3412786282 Optimize used memory by metadictionarys a bit. 2025-05-21 15:45:05 +02:00
Ottermandias
861cbc7759 Add global EQP edits to always hide horns or ears. 2025-05-21 15:45:05 +02:00
Actions User
fefa3852f7 [CI] Updating repo.json for testing_1.3.6.12 2025-05-19 15:17:54 +00:00
Ottermandias
68b68d6ce7 Fix some issues with customization IDs and supported counts. 2025-05-19 17:15:29 +02:00
Ottermandias
47b5895404 Fix issue with temp settings again. 2025-05-18 22:00:17 +02:00
Actions User
e18e4bb0e1 [CI] Updating repo.json for testing_1.3.6.11 2025-05-18 14:02:16 +00:00
Ottermandias
6e4e28fa00 Fix disabling conditional shapes. 2025-05-18 16:00:09 +02:00
Ottermandias
e326e3d809 Update shp conditions. 2025-05-18 15:52:47 +02:00
Ottermandias
fbc4c2d054 Improve option select combo. 2025-05-18 12:54:23 +02:00
Ottermandias
3078c467d0 Fix issue with empty and temporary settings. 2025-05-18 12:54:23 +02:00
Ottermandias
52927ff06b Fix clipping in meta edits. 2025-05-18 12:54:23 +02:00
Actions User
08e8b9d2a4 [CI] Updating repo.json for testing_1.3.6.10 2025-05-15 22:28:36 +00:00
Ottermandias
f1448ed947 Add conditional connector shapes. 2025-05-16 00:25:13 +02:00
Ottermandias
c0dcfdd835 Update shape string format. 2025-05-15 22:23:42 +02:00
Actions User
70295b7a6b [CI] Updating repo.json for testing_1.3.6.9 2025-05-15 15:50:16 +00:00
Ottermandias
480942339f Add draggable mod selector width. 2025-05-15 17:47:32 +02:00
Ottermandias
6ad0b4299a Add shape meta manipulations and rework attribute hook. 2025-05-15 17:46:53 +02:00
Ottermandias
0adec35848 Add initial support for custom shapes. 2025-05-15 00:26:59 +02:00
Ottermandias
0fe4a3671a Improve small issue with redraw service. 2025-05-08 23:46:25 +02:00
Caraxi
363d115be8 Add filter for temporary mods 2025-04-19 23:14:39 +02:00
Ottermandias
7595827d29 Merge branch 'master' of github.com:xivDev/Penumbra 2025-04-19 23:12:02 +02:00
Ottermandias
117724b0ae Update npc names. 2025-04-19 23:11:45 +02:00
Ottermandias
a5d221dc13 Make temporary mode checkbox more visible. 2025-04-18 00:17:07 +02:00
Ottermandias
cbebfe5e99 Fix sizing of mod panel. 2025-04-17 01:06:58 +02:00
Ottermandias
0c768979d4 Don't use DalamudPackager for no reason. 2025-04-17 01:06:22 +02:00
Ottermandias
53ef42adfa Update EST Customization identification. 2025-04-17 01:06:09 +02:00
Ottermandias
0954f50912 Update OtterGui, GameData, Namespaces. 2025-04-17 01:05:56 +02:00
Actions User
5d5fc673b1 [CI] Updating repo.json for 1.3.6.8 2025-04-10 14:42:26 +00:00
Ottermandias
2bd0c89588 Better item sort for item swap selectors. 2025-04-10 16:04:49 +02:00
Ottermandias
f03a139e0e blech 2025-04-10 00:17:23 +02:00
Ottermandias
f9b5a626cf Add some migration stuff. 2025-04-10 00:02:49 +02:00
Ottermandias
dc336569ff Add context to copy the full file path from redirections. 2025-04-10 00:02:36 +02:00
Ottermandias
0ec6a17ac7 Add context to open backup directory. 2025-04-10 00:02:21 +02:00
Ottermandias
129156a1c1 Add some more safety and better IPC for draw object storage. 2025-04-09 15:04:47 +02:00
Ottermandias
33ada1d994 Remove meta-default-value checking from TT imports, move it entirely to mod loads, and keep default-valued entries if other options actually edit the same entry. 2025-04-08 16:56:23 +02:00
Ottermandias
0afcae4504 Run API redraws on framework. 2025-04-05 18:49:30 +02:00
Ottermandias
93e60471de Update for new objectmanager. 2025-04-05 18:49:18 +02:00
Actions User
5437ab477f [CI] Updating repo.json for 1.3.6.7 2025-04-05 12:44:47 +00:00
Ottermandias
3b54485127 Maybe fix AtchCaller crashes. 2025-04-05 14:42:25 +02:00
Ottermandias
c3b2443ab5 Add Incognito modifier. 2025-04-04 22:35:23 +02:00
Actions User
2fdafc5c85 [CI] Updating repo.json for 1.3.6.6 2025-04-02 21:45:19 +00:00
Ottermandias
09c2264de4 Revert overeager BNPC Name update. 2025-04-02 23:41:08 +02:00
Ottermandias
c3be151d40 Maybe fix crash issue in AtchHook1 / issue with kept draw object links. 2025-04-02 23:37:06 +02:00
Exter-N
abb47751c8 Mtrl editor: Disregard obsolete modded ShPks 2025-03-30 20:32:03 +02:00
Exter-N
1d517103b3 Mtrl editor: Fix texture pinning 2025-03-30 20:32:03 +02:00
Actions User
fe5d1bc36e [CI] Updating repo.json for 1.3.6.5 2025-03-30 16:08:59 +00:00
Exter-N
b589103b05 Make resolvedData thread-local 2025-03-30 13:56:32 +02:00
Actions User
cc76125b1c [CI] Updating repo.json for 1.3.6.4 2025-03-29 17:07:46 +00:00
Ottermandias
f3bcc4d554 Update changelog. 2025-03-29 18:05:47 +01:00
Ottermandias
2dd6dd201c Update PAP records. 2025-03-29 18:03:57 +01:00
Exter-N
cb0214ca2f Fix material editor and improve pinning logic 2025-03-29 16:55:44 +01:00
Exter-N
5a5a1487a3 Fix texture naming in Resource Trees 2025-03-29 16:55:44 +01:00
Actions User
de408e4d58 [CI] Updating repo.json for 1.3.6.3 2025-03-28 17:33:26 +00:00
Ottermandias
a1bf26e7e8 Run HTTP redraws on framework thread. 2025-03-28 18:30:26 +01:00
Actions User
3bb7db10fb [CI] Updating repo.json for 1.3.6.2 2025-03-28 16:29:20 +00:00
Ottermandias
8a68a1bff5 Update GameData. 2025-03-28 17:25:03 +01:00
Ottermandias
01e6f58463 Add Launching IPC Event. API 5.8 2025-03-28 16:53:50 +01:00
Actions User
7498bc469f [CI] Updating repo.json for 1.3.6.1 2025-03-28 14:55:12 +00:00
Ottermandias
23ba77c107 Update build step and check for pre 7.2 shps. 2025-03-28 15:52:40 +01:00
Ottermandias
1a1d1c1840 Revert Dalamud staging on release, and update api level. 2025-03-28 14:10:52 +01:00
Actions User
b019da2a8c [CI] Updating repo.json for 1.3.6.0 2025-03-28 13:09:26 +00:00
Ottermandias
60becf0a09 Use staging build for release for now. 2025-03-28 14:06:21 +01:00
Ottermandias
974b215610 1.3.6.0 2025-03-28 13:58:11 +01:00
Ottermandias
8e191ae075 Fix offsets. 2025-03-28 13:33:43 +01:00
Ottermandias
b189ac027b Fix imgui assert. 2025-03-28 02:29:49 +01:00
Ottermandias
6cbc8bd58f Merge remote-tracking branch 'Exter-N/72' 2025-03-28 00:59:14 +01:00
Exter-N
49f077aca0 Fixes for 7.2 (ResourceTree + ShPk 13.1) 2025-03-27 22:32:07 +01:00
Ottermandias
525d1c6bf9 Update GameData. 2025-03-27 18:18:04 +01:00
Ottermandias
124b54ab04 Update GameData. 2025-03-27 16:00:30 +01:00
Ottermandias
b8b2127a5d Update STM and signatures. 2025-03-27 15:53:59 +01:00
Ottermandias
586bd9d0cc Re-add wrong dependencies. 2025-03-27 12:07:45 +01:00
Ottermandias
03bb07a9c0 Update for SDK. 2025-03-27 12:04:38 +01:00
Ottermandias
279a861582 Fix error in parser. 2025-03-16 22:17:37 +01:00
Ottermandias
82a1271281 Add option to import atch files from the mod itself via context. 2025-03-16 15:46:51 +01:00
Ottermandias
26a6cc4735 Fix clipping in changed items panel without grouping. 2025-03-16 15:46:51 +01:00
Ottermandias
61d70f7b4e Fix identification of EST changes. 2025-03-16 15:46:51 +01:00
Ottermandias
0213096c58 Add BodyHideGloveCuffs name to eqp entries. 2025-03-16 15:46:51 +01:00
Actions User
dc47a08988 [CI] Updating repo.json for testing_1.3.5.1 2025-03-13 23:20:54 +00:00
Ottermandias
83574dfeb1 Merge remote-tracking branch 'Exter-N/better-tex' 2025-03-14 00:16:33 +01:00
Ottermandias
87f44d7a88 Some minor parser fixes thanks to Anna. 2025-03-14 00:13:57 +01:00
Ottermandias
cda6a4c420 Make preferred changed item star more noticeable, and make the color configurable. 2025-03-14 00:13:01 +01:00
Exter-N
4093228e61 Improve wording of block compressions (suggested by @Theo-Asterio) 2025-03-12 23:04:57 +01:00
Exter-N
442ae960cf Add encoding support for BC1, BC4 and BC5 2025-03-12 20:03:53 +01:00
Exter-N
e7f7077e96 Simplify passing of the device (suggested by @rootdarkarchon) 2025-03-12 15:18:20 +01:00
Exter-N
e5620e17e0 Improve texture saving 2025-03-12 01:20:36 +01:00
Ottermandias
93b0996794 Add chat command to clear temporary settings. 2025-03-11 18:13:08 +01:00
Actions User
1d70be8060 [CI] Updating repo.json for 1.3.5.0 2025-03-09 22:16:58 +00:00
Ottermandias
eab98ec0e4 1.3.5.0 2025-03-09 14:41:45 +01:00
Ottermandias
861b7b78cd Merge branch 'master' of github.com:xivDev/Penumbra 2025-03-09 13:48:56 +01:00
Ottermandias
6eacc82dcd Update references. 2025-03-09 13:48:41 +01:00
Ottermandias
1afbbfef78 Update NuGet packages. 2025-03-09 13:39:41 +01:00
Ottermandias
7cf0367361 Try moving extracted folders 3 times for unknown issues. 2025-03-09 13:39:25 +01:00
Ottermandias
0b0c92eb09 Some cleanup. 2025-03-02 14:27:40 +01:00
Actions User
34d51b66aa [CI] Updating repo.json for testing_1.3.4.6 2025-03-01 21:37:03 +00:00
Ottermandias
cda9b1df65 Fix weapon identification bug. 2025-03-01 22:34:50 +01:00
Ottermandias
509f11561a Add preferred changed items to mods. 2025-03-01 22:21:36 +01:00
Ottermandias
13adbd5466 Allow configuration of the changed item display. 2025-03-01 16:56:02 +01:00
Actions User
26985e01a2 [CI] Updating repo.json for testing_1.3.4.5 2025-02-28 23:37:03 +00:00
Ottermandias
deba8ac910 Heavily improve changed item display. 2025-03-01 00:33:56 +01:00
Ottermandias
1ebe4099d6 Add ImGuiCacheService. 2025-02-27 17:51:27 +01:00
Ottermandias
c6de7ddebd Improve GamePaths and parsing, add support for identifying skeletons and phybs. 2025-02-27 13:08:41 +01:00
Ottermandias
8860d1e39a Fix an exception in incognito names in weird cutscene cases. 2025-02-27 06:01:08 +01:00
Ottermandias
2413424c8a Merge branch 'rt-more-files' 2025-02-27 05:51:43 +01:00
Ottermandias
9b25193d4e ImUtf8 and null-check cleanup. 2025-02-27 05:51:25 +01:00
Ottermandias
70844610d8 Primary constructor and some null-check cleanup. 2025-02-27 05:45:06 +01:00
Ottermandias
e4cfd674ee Probably unnecessary size optimization. 2025-02-27 05:39:19 +01:00
Ottermandias
776a93dc73 Some null-check cleanup. 2025-02-27 05:38:56 +01:00
Exter-N
514b0e7f30 Add file types to Resource Tree and require Ctrl+Shift for some quick imports 2025-02-27 00:10:24 +01:00
Actions User
4a00d82921 [CI] Updating repo.json for testing_1.3.4.4 2025-02-20 18:20:23 +00:00
Ottermandias
fdd75e2866 Use Meta Compression V1. 2025-02-20 19:17:55 +01:00
Ottermandias
b2860c1047 Merge branch 'refs/heads/adamm789/model-export' 2025-02-20 18:38:21 +01:00
Ottermandias
1f172b4632 Make default constructed models use V6 instead of V5. 2025-02-20 18:37:15 +01:00
Ottermandias
d40c59eee9 Slight cleanup. 2025-02-20 18:36:46 +01:00
Ottermandias
f8d0616acd Notify when an unhandled UV count is reached. 2025-02-20 18:36:33 +01:00
Ottermandias
31f23024a4 Notify and fail when a list of vertex usages has more than one entry where this is not expected. 2025-02-20 18:36:08 +01:00
Adam Moy
6d2b72e079 Removed irrelevant comments 2025-02-20 18:13:53 +01:00
Adam Moy
b76626ac8d Added VertexTexture3
Not sure of accuracy but followed existing pattern
2025-02-20 18:13:53 +01:00
Adam Moy
579969a9e1 Using LINQ
And also change types from using LINQ
2025-02-20 18:13:53 +01:00
Adam Moy
2f0bf19d00 Use First().Value 2025-02-20 18:13:53 +01:00
Adam Moy
ef26049c53 Consider VertexElement's UsageIndex
Allows VertexDeclarations to have multiple VertexElements of the same Type but different UsageIndex
2025-02-20 18:13:53 +01:00
Actions User
a73dee83b3 [CI] Updating repo.json for testing_1.3.4.3 2025-02-18 14:12:54 +00:00
Ottermandias
41672c31ce Update message slightly. 2025-02-18 15:10:16 +01:00
Ottermandias
a561e70410 Add option to always work in temporary settings. 2025-02-17 17:39:48 +01:00
Ottermandias
b7b9defaa6 Add context menu to clear temporary settings. 2025-02-17 17:39:48 +01:00
Actions User
79938b6dd0 [CI] Updating repo.json for testing_1.3.4.2 2025-02-15 15:50:18 +00:00
Ottermandias
40f24344af Update OtterGui. 2025-02-15 16:46:39 +01:00
Ottermandias
93e184c9a5 Add import of .atch files into metadata. 2025-02-15 16:46:39 +01:00
Ottermandias
2be5bd0611 Make EQP swaps also swap multi-slot items correctly. 2025-02-15 16:46:39 +01:00
Actions User
f89eab8b2b [CI] Updating repo.json for testing_1.3.4.1 2025-02-13 15:42:58 +00:00
Ottermandias
a9a556eb55 Add CheckCurrentChangedItemFunc, 2025-02-13 13:43:54 +01:00
Ottermandias
0af9667789 Add changed item adapters. 2025-02-12 16:44:22 +01:00
Ottermandias
60b9facea3 Cont. 2025-02-09 15:27:32 +01:00
Ottermandias
50c4207844 Give messages for unsupported file redirection types. 2025-02-09 15:12:34 +01:00
Ottermandias
9b18ffce66 Updated submodule Versions. 2025-02-06 16:58:48 +01:00
Actions User
214be98662 [CI] Updating repo.json for 1.3.4.0 2025-02-06 15:55:30 +00:00
Ottermandias
f9952ada75 1.3.4.0 2025-02-06 16:22:08 +01:00
Ottermandias
3ba2563e0b Merge branch 'master' of github.com:xivDev/Penumbra 2025-02-03 17:43:49 +01:00
Ottermandias
4cc5041f0a Improve cleanup. 2025-02-03 17:43:44 +01:00
Exter-N
f9b163e7c5 Add explanations on why paths are redacted 2025-02-03 17:13:04 +01:00
Exter-N
981c2bace4 Fix out-of-root path detection logic 2025-02-03 17:13:04 +01:00
Ottermandias
ec09a7eb0e Add initial cleaning functions, to be improved. 2025-01-31 18:46:17 +01:00
Ottermandias
7022b37043 Add some improved Mod Setting API. 2025-01-31 15:31:05 +01:00
Ottermandias
64748790cc Make limits a bit cleaner. 2025-01-30 14:06:39 +01:00
Theo
b0a8b1baa5 Bone and Material Limit updates.
Fix UI in Models tab to allow for more than 4 Materials per DT spec.
2025-01-30 13:57:43 +01:00
Actions User
ac64b4db24 [CI] Updating repo.json for testing_1.3.3.10 2025-01-25 13:01:04 +00:00
Ottermandias
e508e6158f Merge branch 'async-stuff'
# Conflicts:
#	Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs
2025-01-25 13:55:54 +01:00
Ottermandias
4d26a63944 Test disabling MtrlForceSync. 2025-01-25 13:55:13 +01:00
Ottermandias
30a957356a Minor changes. 2025-01-25 13:54:19 +01:00
Ottermandias
9ab8985343 Debug logging. 2025-01-25 12:38:10 +01:00
Exter-N
a3ddce0ef5 Add mechanism to handle completion of async res loads 2025-01-24 02:50:02 +01:00
Actions User
0159eb3d83 [CI] Updating repo.json for testing_1.3.3.9 2025-01-23 17:13:52 +00:00
Ottermandias
55ce633832 Try forcing IMC files to load synchronously for now. 2025-01-23 18:11:35 +01:00
Ottermandias
40168d7daf Fix issue with IPC adding mods before character utility is ready in rare cases. 2025-01-22 23:14:09 +01:00
Actions User
dcab443b2f [CI] Updating repo.json for testing_1.3.3.8 2025-01-22 16:38:33 +00:00
Ottermandias
dcc4354777 Fix clipping height in changed items tab. 2025-01-22 17:36:13 +01:00
Ottermandias
2afd6b966e Add debug logging facilities. 2025-01-22 17:36:01 +01:00
Actions User
737e74582b [CI] Updating repo.json for testing_1.3.3.7 2025-01-20 19:36:05 +00:00
Ottermandias
39c73af238 Fix stupid. 2025-01-20 20:33:35 +01:00
Actions User
9ca0145a7f [CI] Updating repo.json for testing_1.3.3.6 2025-01-20 16:48:19 +00:00
Ottermandias
0c8571fba9 Reduce and pad IMC allocations and log allocations. 2025-01-20 17:14:13 +01:00
Ottermandias
8779f4b689 Add new cutscene ENPC tracking hooks. 2025-01-20 15:36:05 +01:00
Ottermandias
7b517390b6 Fix temporary settings causing collection saves. 2025-01-20 15:30:03 +01:00
Ottermandias
7d75c7d7a5 Merge branch 'json-schema' 2025-01-20 15:13:20 +01:00
Ottermandias
4f0428832c Fix solution file for schemas. 2025-01-20 15:13:09 +01:00
Exter-N
b62563d721 Remove $id from shpk_devkit schema 2025-01-17 20:06:21 +01:00
Ottermandias
ec3ec7db4e update schema organization anf change some things. 2025-01-17 19:55:02 +01:00
Ottermandias
5f8377acaa Update mod loading structure. 2025-01-17 19:54:40 +01:00
Exter-N
3b8aac8eca Add schema for Material Development Kit files 2025-01-17 18:50:00 +01:00
Exter-N
a1931a93fb Add drafts of JSON schemas 2025-01-17 01:45:37 +01:00
Actions User
df148b556a [CI] Updating repo.json for testing_1.3.3.5 2025-01-16 16:25:18 +00:00
Ottermandias
bdc2da95c4 Make mods write empty containers again for now. 2025-01-16 17:22:25 +01:00
Actions User
1462891bd3 [CI] Updating repo.json for testing_1.3.3.4 2025-01-15 17:05:31 +00:00
Ottermandias
d2a8cec01f Merge branch 'combining' 2025-01-15 17:44:58 +01:00
Ottermandias
795fa7336e Update with workable prototype. 2025-01-15 17:44:22 +01:00
Ottermandias
e77fa18c61 Start for combining groups. 2025-01-15 14:25:15 +01:00
Ottermandias
9559bd7358 Improve RSP Identifier ToString. 2025-01-14 15:20:37 +01:00
Ottermandias
9c25fab183 Increase API minor version. 2025-01-14 14:41:32 +01:00
Ottermandias
cc981eba15 Fix used dye channel in material editor previews. 2025-01-14 14:34:34 +01:00
Ottermandias
30a4b90e84 Add IPC for querying temporary settings. 2025-01-14 14:34:18 +01:00
Ottermandias
82689467aa Add counts to multi mod selection. 2025-01-12 00:03:36 +01:00
Ottermandias
415e15f3b1 Fix another issue with temporary mod settings. 2025-01-11 21:12:21 +01:00
Actions User
3687c99ee6 [CI] Updating repo.json for testing_1.3.3.3 2025-01-11 17:02:43 +00:00
Ottermandias
6ea38eac0a Share PeSigScanner and use in RenderTargetHdrEnabler because of ReShade. 2025-01-11 17:59:50 +01:00
Actions User
7f52777fd4 [CI] Updating repo.json for testing_1.3.3.2 2025-01-11 14:58:57 +00:00
Ottermandias
2753c786fc Only put out warnings if the path is rooted. 2025-01-11 15:55:14 +01:00
Ottermandias
7b2e82b27f Add some HDR related debug data and support info. 2025-01-11 15:22:04 +01:00
Ottermandias
aebd22ed64 Merge branch 'rt-hdr' 2025-01-11 14:21:12 +01:00
Actions User
c99a7884bb [CI] Updating repo.json for 1.3.3.1 2025-01-11 12:49:05 +00:00
Ottermandias
e73b3e85bd Autoformat and remove nagging. 2025-01-11 13:46:44 +01:00
Ottermandias
0758739666 Cleanup UI code. 2025-01-11 13:46:08 +01:00
Ottermandias
d4e6688369 Fix issue when empty settings are turned temporary. 2025-01-11 13:26:51 +01:00
Actions User
e6872cff64 [CI] Updating repo.json for 1.3.3.0 2025-01-10 19:00:27 +00:00
Ottermandias
b83564bce8 1.3.3.0 2025-01-10 19:55:33 +01:00
Exter-N
e8300fc5c8 Improve RT-HDR texture comments 2025-01-09 20:42:48 +01:00
Exter-N
f07780cf7b Add RenderTargetHdrEnabler 2025-01-08 20:02:14 +01:00
Ottermandias
349241d0ab Better attribution of authors in item swap. 2025-01-07 16:49:19 +01:00
Ottermandias
1845c4b89b Merge branch 'master' of github.com:xivDev/Penumbra 2025-01-07 16:11:38 +01:00
Ottermandias
756537c776 Add Turn Permanent button for temporary settings and improve buttons, make secure. 2025-01-07 16:11:26 +01:00
Ottermandias
9a457a1a95 Add debug panel to check changed item identification for paths. 2025-01-07 16:11:05 +01:00
Ottermandias
af7a8fbddd Fix bug with atch counter. 2025-01-07 16:10:37 +01:00
N. Lo.
0eed5f1707 Add a watched plugin to Support Info 2025-01-06 17:56:58 +01:00
Actions User
6374362b28 [CI] Updating repo.json for testing_1.3.2.2 2024-12-31 17:05:32 +00:00
Ottermandias
7da5d73b47 Keep enabled and priority at the top of settings, add button to turn temporary. 2024-12-31 17:56:58 +01:00
Ottermandias
a2258e6160 Add some temporary context menu things. 2024-12-31 17:54:16 +01:00
Ottermandias
dbef1cccb2 Fix stuff after submodule update. 2024-12-31 17:10:09 +01:00
Ottermandias
653f6269b7 Update submodule. 2024-12-31 16:38:15 +01:00
Ottermandias
a5d8baebca Merge branch 'TempSettings' 2024-12-31 16:36:59 +01:00
Ottermandias
cff482a2ed Allow non-locking, negative identifier-locks 2024-12-31 16:36:46 +01:00
Ottermandias
5f9cbe9ab1 Current State. 2024-12-31 15:40:25 +01:00
Ottermandias
282189ef6d Current State. 2024-12-31 15:40:25 +01:00
Ottermandias
98a89bb2b4 Current state. 2024-12-31 15:40:25 +01:00
Ottermandias
67305d507a Extract ModCollectionIdentity. 2024-12-31 15:40:24 +01:00
Ottermandias
fbbfe5e00d Extract collection counters. 2024-12-31 15:40:24 +01:00
Ottermandias
7a2691b942 Add colors for temporary settings. 2024-12-31 15:40:24 +01:00
Ottermandias
50b5eeb700 Add FullModSettings struct. 2024-12-31 15:40:24 +01:00
Ottermandias
2483f3dcdf Add Temporary Settings class 2024-12-31 15:40:24 +01:00
Ottermandias
0e2364497f Maybe fix mtrl file issues. 2024-12-30 00:33:46 +01:00
Ottermandias
25d0a2c9a8 Fix issue with ring IMCs in resource tree. 2024-12-29 23:04:43 +01:00
Actions User
f24056ea31 [CI] Updating repo.json for testing_1.3.2.1 2024-12-25 23:08:52 +00:00
Ottermandias
b3883c1306 Add handling for cached TMBs. 2024-12-26 00:06:51 +01:00
Ottermandias
f679e0ccee Fix some imgui assertions. 2024-12-25 22:22:30 +01:00
Ottermandias
d5e575423b Merge branch 'master' of github.com:xivDev/Penumbra 2024-12-17 18:04:34 +01:00
Ottermandias
18288815b2 Add partial copying of color and colordye tables. 2024-12-17 18:04:17 +01:00
Ottermandias
cc97ea0ce9 Add an option to automatically select the collection assigned to the current character on login. 2024-12-16 17:52:57 +01:00
Actions User
b5a469c524 [CI] Updating repo.json for 1.3.2.0 2024-12-13 17:13:38 +00:00
Ottermandias
510b9a5f1f 1.3.2.0 2024-12-13 18:11:17 +01:00
Ottermandias
5db3d53994 Small improvements. 2024-12-13 17:56:27 +01:00
Ottermandias
08ff9b679e Add changing mod settings to command / macro API. 2024-12-13 17:48:54 +01:00
Ottermandias
22c3b3b629 Again. 2024-12-13 15:43:09 +01:00
Ottermandias
4cc7d1930b Update GameData. 2024-12-09 21:30:00 +01:00
Ottermandias
01db37cbd4 Add Copy for paths, update npc names 2024-12-09 21:22:30 +01:00
Actions User
e9014fe4c3 [CI] Updating repo.json for testing_1.3.1.6 2024-12-05 19:38:51 +00:00
Ottermandias
1434ad6190 Add context menu copying for paths in advanced editing. 2024-12-05 20:36:03 +01:00
Ottermandias
22541b3fd8 Update variables drawer. 2024-12-05 20:36:03 +01:00
Actions User
b377ca372c [CI] Updating repo.json for testing_1.3.1.5 2024-11-29 16:35:08 +00:00
Ottermandias
d7095af89b Add jumping to mods in OnScreen tab. 2024-11-29 17:33:05 +01:00
Ottermandias
8b9f59426e No V1 Meta yet... wait until next version ban or API increase. 2024-11-27 23:07:08 +01:00
Ottermandias
97d7ea7759 tmp 2024-11-27 22:51:14 +01:00
Ottermandias
9787e5a852 Fix some meta issues. 2024-11-27 18:49:04 +01:00
Ottermandias
242c0ee38f Add testing to IPC Meta. 2024-11-27 18:41:16 +01:00
Ottermandias
c8ad4bc106 Use meta transfer v1. 2024-11-27 18:01:53 +01:00
Ottermandias
ac2631384f Fix mod reload of atch manipulations. 2024-11-27 18:01:53 +01:00
Ottermandias
0aa8a44b8d Fix meta manipulation copy/paste. 2024-11-27 18:01:52 +01:00
Ottermandias
8242cde15c Don't spam logs. 2024-11-27 18:01:52 +01:00
Actions User
28250a9304 [CI] Updating repo.json for testing_1.3.1.4 2024-11-26 16:11:29 +00:00
Ottermandias
10279fdc18 fix inverted hook logic. 2024-11-26 17:09:14 +01:00
Ottermandias
b1be868a6a Atch stuff. 2024-11-26 17:09:14 +01:00
Ottermandias
65538868c3 Add Artemis 2024-11-26 17:09:14 +01:00
Actions User
cc49bdcb36 [CI] Updating repo.json for 1.3.1.3 2024-11-26 01:02:41 +00:00
373 changed files with 17039 additions and 3428 deletions

View file

@ -3576,6 +3576,18 @@ resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_
resharper_xaml_x_key_attribute_disallowed_highlighting=error
resharper_xml_doc_comment_syntax_problem_highlighting=warning
resharper_xunit_xunit_test_with_console_output_highlighting=warning
csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion
csharp_style_expression_bodied_methods = true:silent
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_expression_bodied_constructors = true:silent
csharp_style_expression_bodied_operators = true:silent
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_style_expression_bodied_properties = true:silent
[*.{cshtml,htm,html,proto,razor}]
indent_style=tab

View file

@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.x.x'
dotnet-version: '9.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud

View file

@ -15,12 +15,12 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.x.x'
dotnet-version: '9.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |

View file

@ -15,7 +15,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.x.x'
dotnet-version: '9.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud

@ -1 +1 @@
Subproject commit 215e01722a319c70b271dd23a40d99edc3fc197e
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770

@ -1 +1 @@
Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf

View file

@ -1,4 +1,6 @@
using System.Text.Json.Nodes;
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;

View file

@ -1,4 +1,6 @@
using System.Text.Json.Nodes;
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;

View file

@ -1,5 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Numerics;
using System.Text;

View file

@ -1,4 +1,6 @@
using System.Text.Json.Nodes;
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;

View file

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;

View file

@ -1,4 +1,7 @@
using System.Text.Json.Nodes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;

View file

@ -1,4 +1,5 @@
using Penumbra.CrashHandler.Buffers;
using System;
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;

View file

@ -1,20 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x64</PlatformTarget>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Linux'))">$(HOME)/.xlcore/dalamud/Hooks/dev/</DalamudLibPath>
<DalamudLibPath Condition="$(DALAMUD_HOME) != ''">$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -25,4 +11,8 @@
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup>
<Use_DalamudPackager>false</Use_DalamudPackager>
</PropertyGroup>
</Project>

View file

@ -1,4 +1,6 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
namespace Penumbra.CrashHandler;

View file

@ -0,0 +1,13 @@
{
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.2.25, )",
"resolved": "1.2.25",
"contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg=="
}
}
}
}

@ -1 +1 @@
Subproject commit 07d18f7f7218811956e6663592e53c4145f2d862
Subproject commit d889f9ef918514a46049725052d378b441915b00

@ -1 +1 @@
Subproject commit dd83f97299ac33cfacb1064bde4f4d1f6a260936
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793

View file

@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\build.yml = .github\workflows\build.yml
Penumbra\Penumbra.json = Penumbra\Penumbra.json
.github\workflows\release.yml = .github\workflows\release.yml
repo.json = repo.json
.github\workflows\test_release.yml = .github\workflows\test_release.yml
@ -24,40 +25,74 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}"
ProjectSection(SolutionItems) = preProject
schemas\default_mod.json = schemas\default_mod.json
schemas\group.json = schemas\group.json
schemas\local_mod_data-v3.json = schemas\local_mod_data-v3.json
schemas\mod_meta-v3.json = schemas\mod_meta-v3.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}"
ProjectSection(SolutionItems) = preProject
schemas\structs\container.json = schemas\structs\container.json
schemas\structs\group_combining.json = schemas\structs\group_combining.json
schemas\structs\group_imc.json = schemas\structs\group_imc.json
schemas\structs\group_multi.json = schemas\structs\group_multi.json
schemas\structs\group_single.json = schemas\structs\group_single.json
schemas\structs\manipulation.json = schemas\structs\manipulation.json
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
schemas\structs\meta_est.json = schemas\structs\meta_est.json
schemas\structs\meta_geqp.json = schemas\structs\meta_geqp.json
schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
schemas\structs\meta_shp.json = schemas\structs\meta_shp.json
schemas\structs\option.json = schemas\structs\option.json
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BFEA7504-1210-4F79-A7FE-BF03B6567E33} = {F89C9EAE-25C8-43BE-8108-5921E5A93502}
{B03F276A-0572-4F62-AF86-EF62F6B80463} = {BFEA7504-1210-4F79-A7FE-BF03B6567E33}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
EndGlobalSection

View file

@ -2,13 +2,14 @@ using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
namespace Penumbra.Api.Api;
public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService
{
public Dictionary<Guid, string> GetCollections()
=> collections.Storage.ToDictionary(c => c.Id, c => c.Name);
=> collections.Storage.ToDictionary(c => c.Identity.Id, c => c.Identity.Name);
public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier)
{
@ -17,17 +18,33 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
var list = new List<(Guid Id, string Name)>(4);
if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty)
list.Add((collection.Id, collection.Name));
list.Add((collection.Identity.Id, collection.Identity.Name));
else if (identifier.Length >= 8)
list.AddRange(collections.Storage.Where(c => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase))
.Select(c => (c.Id, c.Name)));
list.AddRange(collections.Storage.Where(c => c.Identity.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase))
.Select(c => (c.Identity.Id, c.Identity.Name)));
list.AddRange(collections.Storage
.Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name)))
.Select(c => (c.Id, c.Name)));
.Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase)
&& !list.Contains((c.Identity.Id, c.Identity.Name)))
.Select(c => (c.Identity.Id, c.Identity.Name)));
return list;
}
public Func<string, (string ModDirectory, string ModName)[]> CheckCurrentChangedItemFunc()
{
var weakRef = new WeakReference<CollectionManager>(collections);
return s =>
{
if (!weakRef.TryGetTarget(out var c))
throw new ObjectDisposedException("The underlying collection storage of this IPC container was disposed.");
if (!c.Active.Current.ChangedItems.TryGetValue(s, out var d))
return [];
return d.Item1.Select(m => (m is Mod mod ? mod.Identifier : string.Empty, m.Name.Text)).ToArray();
};
}
public Dictionary<string, object?> GetChangedItemsForCollection(Guid collectionId)
{
try
@ -54,7 +71,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
return null;
var collection = collections.Active.ByType((CollectionType)type);
return collection == null ? null : (collection.Id, collection.Name);
return collection == null ? null : (collection.Identity.Id, collection.Identity.Name);
}
internal (Guid Id, string Name)? GetCollection(byte type)
@ -64,17 +81,18 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name));
return (false, false, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
if (collections.Active.Individuals.TryGetValue(id, out var collection))
return (true, true, (collection.Id, collection.Name));
return (true, true, (collection.Identity.Id, collection.Identity.Name));
helpers.AssociatedCollection(gameObjectIdx, out collection);
return (true, false, (collection.Id, collection.Name));
return (true, false, (collection.Identity.Id, collection.Identity.Name));
}
public Guid[] GetCollectionByName(string name)
=> collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray();
=> collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id)
.ToArray();
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId,
bool allowCreateNew, bool allowDelete)
@ -83,7 +101,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
return (PenumbraApiEc.InvalidArgument, null);
var oldCollection = collections.Active.ByType((CollectionType)type);
var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple<Guid, string>?();
var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
@ -106,7 +124,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
collections.Active.CreateSpecialCollection((CollectionType)type);
}
else if (old.Value.Item1 == collection.Id)
else if (old.Value.Item1 == collection.Identity.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
@ -120,10 +138,10 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name));
return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null;
var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple<Guid, string>?();
var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
@ -148,7 +166,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
var ids = collections.Active.Individuals.GetGroup(id);
collections.Active.CreateIndividualCollection(ids);
}
else if (old.Value.Item1 == collection.Id)
else if (old.Value.Item1 == collection.Identity.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}

View file

@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly CutsceneService _cutsceneService;
private readonly ResourceLoader _resourceLoader;
public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
ResourceLoader resourceLoader)
ResourceLoader resourceLoader, DrawObjectState drawObjectState)
{
_communicator = communicator;
_collectionResolver = collectionResolver;
_cutsceneService = cutsceneService;
_resourceLoader = resourceLoader;
_drawObjectState = drawObjectState;
_resourceLoader.ResourceLoaded += OnResourceLoaded;
_resourceLoader.PapRequested += OnPapRequested;
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
@ -61,12 +63,36 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject)
{
var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name));
return (data.AssociatedGameObject, (Id: data.ModCollection.Identity.Id, Name: data.ModCollection.Identity.Name));
}
public int GetCutsceneParentIndex(int actorIdx)
=> _cutsceneService.GetParentIndex(actorIdx);
public Func<int, int> GetCutsceneParentIndexFunc()
{
var weakRef = new WeakReference<CutsceneService>(_cutsceneService);
return idx =>
{
if (!weakRef.TryGetTarget(out var c))
throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed.");
return c.GetParentIndex(idx);
};
}
public Func<nint, nint> GetGameObjectFromDrawObjectFunc()
{
var weakRef = new WeakReference<DrawObjectState>(_drawObjectState);
return model =>
{
if (!weakRef.TryGetTarget(out var c))
throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed.");
return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero;
};
}
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
? PenumbraApiEc.Success
@ -93,5 +119,5 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
}
private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject)
=> CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject);
=> CreatedCharacterBase?.Invoke(gameObject, collection.Identity.Id, drawObject);
}

View file

@ -0,0 +1,7 @@
namespace Penumbra.Api.Api;
public static class IdentityChecker
{
public static bool Check(string identity)
=> true;
}

View file

@ -5,6 +5,7 @@ using OtterGui;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Files.Utility;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
@ -15,8 +16,6 @@ namespace Penumbra.Api.Api;
public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers)
: IPenumbraApiMeta, IApiService
{
public const int CurrentVersion = 1;
public string GetPlayerMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
@ -52,7 +51,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
}
internal static string CompressMetaManipulations(ModCollection collection)
=> CompressMetaManipulationsV0(collection);
=> CompressMetaManipulationsV1(collection);
private static string CompressMetaManipulationsV0(ModCollection collection)
{
@ -66,6 +65,9 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair<ShpIdentifier, ShpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair<AtrIdentifier, AtrEntry>(kvp.Key, kvp.Value.Entry)));
}
return Functions.ToCompressedBase64(array, 0);
@ -109,6 +111,10 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
{
cache.GlobalEqp.ExitReadLock();
}
WriteCache(zipStream, cache.Atch);
WriteCache(zipStream, cache.Shp);
WriteCache(zipStream, cache.Atr);
}
}
@ -138,16 +144,97 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
}
}
public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8);
public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8);
public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P';
public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8);
public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8);
public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8);
public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P';
public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H';
public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8);
public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8);
private static unsafe string CompressMetaManipulationsV2(ModCollection? collection)
{
using var ms = new MemoryStream();
ms.Capacity = 1024;
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
{
zipStream.Write((byte)2);
zipStream.Write("META0002"u8);
if (collection?.MetaCache is { } cache)
{
WriteCache(zipStream, cache.Imc, ImcKey);
WriteCache(zipStream, cache.Eqp, EqpKey);
WriteCache(zipStream, cache.Eqdp, EqdpKey);
WriteCache(zipStream, cache.Est, EstKey);
WriteCache(zipStream, cache.Rsp, RspKey);
WriteCache(zipStream, cache.Gmp, GmpKey);
cache.GlobalEqp.EnterReadLock();
try
{
if (cache.GlobalEqp.Count > 0)
{
zipStream.Write(GeqpKey);
zipStream.Write(cache.GlobalEqp.Count);
foreach (var (globalEqp, _) in cache.GlobalEqp)
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
}
}
finally
{
cache.GlobalEqp.ExitReadLock();
}
WriteCache(zipStream, cache.Atch, AtchKey);
WriteCache(zipStream, cache.Shp, ShpKey);
WriteCache(zipStream, cache.Atr, AtrKey);
}
}
ms.Flush();
ms.Position = 0;
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
return Convert.ToBase64String(data);
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache, uint label)
where TKey : unmanaged, IMetaIdentifier
where TValue : unmanaged
{
metaCache.EnterReadLock();
try
{
if (metaCache.Count <= 0)
return;
stream.Write(label);
stream.Write(metaCache.Count);
foreach (var (identifier, (_, value)) in metaCache)
{
stream.Write(identifier);
stream.Write(value);
}
}
finally
{
metaCache.ExitReadLock();
}
}
}
/// <summary>
/// Convert manipulations from a transmitted base64 string to actual manipulations.
/// The empty string is treated as an empty set.
/// Only returns true if all conversions are successful and distinct.
/// </summary>
internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips)
internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips, out byte version)
{
if (manipString.Length == 0)
{
manips = new MetaDictionary();
manips = new MetaDictionary();
version = byte.MaxValue;
return true;
}
@ -160,13 +247,14 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
zipStream.CopyTo(resultStream);
resultStream.Flush();
resultStream.Position = 0;
var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length);
var version = data[0];
data = data[1..];
var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length);
version = data[0];
data = data[1..];
switch (version)
{
case 0: return ConvertManipsV0(data, out manips);
case 1: return ConvertManipsV1(data, out manips);
case 2: return ConvertManipsV2(data, out manips);
default:
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
manips = null;
@ -176,9 +264,135 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
catch (Exception ex)
{
Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}");
manips = null;
version = byte.MaxValue;
return false;
}
}
private static bool ConvertManipsV2(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (!data.StartsWith("META0002"u8))
{
Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix.");
manips = null;
return false;
}
manips = new MetaDictionary();
var r = new SpanBinaryReader(data[8..]);
while (r.Remaining > 4)
{
var prefix = r.ReadUInt32();
var count = r.Remaining > 4 ? r.ReadInt32() : 0;
if (count is 0)
continue;
switch (prefix)
{
case ImcKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<ImcIdentifier>();
var value = r.Read<ImcEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EqpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EqpIdentifier>();
var value = r.Read<EqpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EqdpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EqdpIdentifier>();
var value = r.Read<EqdpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EstKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EstIdentifier>();
var value = r.Read<EstEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case RspKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<RspIdentifier>();
var value = r.Read<RspEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case GmpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<GmpIdentifier>();
var value = r.Read<GmpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case GeqpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<GlobalEqpManipulation>();
if (!identifier.Validate() || !manips.TryAdd(identifier))
return false;
}
break;
case AtchKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<AtchIdentifier>();
var value = r.Read<AtchEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case ShpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<ShpIdentifier>();
var value = r.Read<ShpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case AtrKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<AtrIdentifier>();
var value = r.Read<AtrEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
}
}
return true;
}
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
@ -254,6 +468,41 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
return false;
}
// Atch was added after there were already some V1 around, so check for size here.
if (r.Position < r.Count)
{
var atchCount = r.ReadInt32();
for (var i = 0; i < atchCount; ++i)
{
var identifier = r.Read<AtchIdentifier>();
var value = r.Read<AtchEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
// Shp and Atr was added later
if (r.Position < r.Count)
{
var shpCount = r.ReadInt32();
for (var i = 0; i < shpCount; ++i)
{
var identifier = r.Read<ShpIdentifier>();
var value = r.Read<ShpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var atrCount = r.ReadInt32();
for (var i = 0; i < atrCount; ++i)
{
var identifier = r.Read<AtrIdentifier>();
var value = r.Read<AtrEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
}
}
return true;
}
@ -262,7 +511,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
var json = Encoding.UTF8.GetString(data);
manips = JsonConvert.DeserializeObject<MetaDictionary>(json);
return manips != null;
}
}
internal void TestMetaManipulations()
{
@ -279,11 +528,11 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
var v1Time = watch.ElapsedMilliseconds;
watch.Restart();
var v1Success = ConvertManips(v1, out var v1Roundtrip);
var v1Success = ConvertManips(v1, out var v1Roundtrip, out _);
var v1RoundtripTime = watch.ElapsedMilliseconds;
watch.Restart();
var v0Success = ConvertManips(v0, out var v0Roundtrip);
var v0Success = ConvertManips(v0, out var v0Roundtrip, out _);
var v0RoundtripTime = watch.ElapsedMilliseconds;
Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal");

View file

@ -1,4 +1,4 @@
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
@ -63,13 +63,18 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
return new AvailableModSettings(dict);
}
public Dictionary<string, (string[], int)>? GetAvailableModSettingsBase(string modDirectory, string modName)
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type))
: null;
public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory,
string modName, bool ignoreInheritance)
{
var ret = GetCurrentModSettingsWithTemp(collectionId, modDirectory, modName, ignoreInheritance, true, 0);
if (ret.Item2 is null)
return (ret.Item1, null);
return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4));
}
public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId,
string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return (PenumbraApiEc.ModMissing, null);
@ -77,17 +82,32 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return (PenumbraApiEc.CollectionMissing, null);
var settings = collection.Id == Guid.Empty
? null
: ignoreInheritance
? collection.Settings[mod.Index]
: collection[mod.Index].Settings;
if (settings == null)
if (collection.Identity.Id == Guid.Empty)
return (PenumbraApiEc.Success, null);
var (enabled, priority, dict) = settings.ConvertToShareable(mod);
return (PenumbraApiEc.Success,
(enabled, priority.Value, dict, collection.Settings[mod.Index] == null));
if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings)
return (PenumbraApiEc.Success, settings);
return (PenumbraApiEc.Success, null);
}
public (PenumbraApiEc, Dictionary<string, (bool, int, Dictionary<string, List<string>>, bool, bool)>?) GetAllModSettings(Guid collectionId,
bool ignoreInheritance, bool ignoreTemporary, int key)
{
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return (PenumbraApiEc.CollectionMissing, null);
if (collection.Identity.Id == Guid.Empty)
return (PenumbraApiEc.Success, []);
var ret = new Dictionary<string, (bool, int, Dictionary<string, List<string>>, bool, bool)>(_modManager.Count);
foreach (var mod in _modManager)
{
if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings)
ret[mod.Identifier] = settings;
}
return (PenumbraApiEc.Success, ret);
}
public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit)
@ -184,36 +204,9 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
if (groupIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
var setting = Setting.Zero;
switch (mod.Groups[groupIdx])
{
case { Behaviour: GroupDrawBehaviour.SingleSelection } single:
{
var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]);
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
setting = Setting.Single(optionIdx);
break;
}
case { Behaviour: GroupDrawBehaviour.MultiSelection } multi:
{
foreach (var name in optionNames)
{
var optionIdx = multi.Options.IndexOf(o => o.Name == name);
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
setting |= Setting.Multi(optionIdx);
}
break;
}
}
var settingSuccess = ConvertModSetting(mod, optionGroupName, optionNames, out var groupIdx, out var setting);
if (settingSuccess is not PenumbraApiEc.Success)
return ApiHelpers.Return(settingSuccess, args);
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
? PenumbraApiEc.Success
@ -238,13 +231,38 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (bool, int, Dictionary<string, List<string>>, bool, bool)? GetCurrentSettings(ModCollection collection, Mod mod,
bool ignoreInheritance, bool ignoreTemporary, int key)
{
var settings = collection.Settings.Settings[mod.Index];
if (!ignoreTemporary && settings.TempSettings is { } tempSettings && (tempSettings.Lock <= 0 || tempSettings.Lock == key))
{
if (!tempSettings.ForceInherit)
return (tempSettings.Enabled, tempSettings.Priority.Value, tempSettings.ConvertToShareable(mod).Settings,
false, true);
if (!ignoreInheritance && collection.GetActualSettings(mod.Index).Settings is { } actualSettingsTemp)
return (actualSettingsTemp.Enabled, actualSettingsTemp.Priority.Value,
actualSettingsTemp.ConvertToShareable(mod).Settings, true, true);
}
if (settings.Settings is { } ownSettings)
return (ownSettings.Enabled, ownSettings.Priority.Value, ownSettings.ConvertToShareable(mod).Settings, false,
false);
if (!ignoreInheritance && collection.GetInheritedSettings(mod.Index).Settings is { } actualSettings)
return (actualSettings.Enabled, actualSettings.Priority.Value,
actualSettings.ConvertToShareable(mod).Settings, true, false);
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void TriggerSettingEdited(Mod mod)
{
var collection = _collectionResolver.PlayerCollection();
var (settings, parent) = collection[mod.Index];
var (settings, parent) = collection.GetActualSettings(mod.Index);
if (settings is { Enabled: true })
ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection);
ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection);
}
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
@ -254,7 +272,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited)
=> ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited);
=> ModSettingChanged?.Invoke(type, collection.Identity.Id, mod?.ModPath.Name ?? string.Empty, inherited);
private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int moveIndex)
@ -283,4 +301,41 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
TriggerSettingEdited(mod);
}
public static PenumbraApiEc ConvertModSetting(Mod mod, string groupName, IReadOnlyList<string> optionNames, out int groupIndex,
out Setting setting)
{
groupIndex = mod.Groups.IndexOf(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase));
setting = Setting.Zero;
if (groupIndex < 0)
return PenumbraApiEc.OptionGroupMissing;
switch (mod.Groups[groupIndex])
{
case { Behaviour: GroupDrawBehaviour.SingleSelection } single:
{
var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]);
if (optionIdx < 0)
return PenumbraApiEc.OptionMissing;
setting = Setting.Single(optionIdx);
break;
}
case { Behaviour: GroupDrawBehaviour.MultiSelection } multi:
{
foreach (var name in optionNames)
{
var optionIdx = multi.Options.IndexOf(o => o.Name == name);
if (optionIdx < 0)
return PenumbraApiEc.OptionMissing;
setting |= Setting.Multi(optionIdx);
}
break;
}
}
return PenumbraApiEc.Success;
}
}

View file

@ -1,3 +1,4 @@
using Newtonsoft.Json.Linq;
using OtterGui.Compression;
using OtterGui.Services;
using Penumbra.Api.Enums;
@ -33,12 +34,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
{
switch (type)
{
case ModPathChangeType.Deleted when oldDirectory != null:
ModDeleted?.Invoke(oldDirectory.Name);
break;
case ModPathChangeType.Added when newDirectory != null:
ModAdded?.Invoke(newDirectory.Name);
break;
case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break;
case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break;
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
break;
@ -46,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
}
public void Dispose()
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
}
public Dictionary<string, string> GetModList()
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
@ -109,10 +108,22 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
public event Action<string>? ModAdded;
public event Action<string, string>? ModMoved;
public event Action<JObject, ushort, string>? CreatingPcp
{
add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi);
remove => _communicator.PcpCreation.Unsubscribe(value!);
}
public event Action<JObject, string, Guid>? ParsingPcp
{
add => _communicator.PcpParsing.Subscribe(value!, PcpParsing.Priority.ModsApi);
remove => _communicator.PcpParsing.Unsubscribe(value!);
}
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.FindLeaf(mod, out var leaf))
|| !_modFileSystem.TryGetValue(mod, out var leaf))
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
var fullPath = leaf.FullName();
@ -127,7 +138,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
return PenumbraApiEc.InvalidArgument;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.FindLeaf(mod, out var leaf))
|| !_modFileSystem.TryGetValue(mod, out var leaf))
return PenumbraApiEc.ModMissing;
try
@ -145,4 +156,10 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject())
: [];
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>> GetChangedItemAdapterDictionary()
=> new ModChangedItemAdapter(new WeakReference<ModStorage>(_modManager));
public IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)> GetChangedItemAdapterList()
=> new ModChangedItemAdapter(new WeakReference<ModStorage>(_modManager));
}

View file

@ -16,13 +16,16 @@ public class PenumbraApi(
TemporaryApi temporary,
UiApi ui) : IDisposable, IApiService, IPenumbraApi
{
public const int BreakingVersion = 5;
public const int FeatureVersion = 13;
public void Dispose()
{
Valid = false;
}
public (int Breaking, int Feature) ApiVersion
=> (5, 3);
=> (BreakingVersion, FeatureVersion);
public bool Valid { get; private set; } = true;
public IPenumbraApiCollection Collection { get; } = collection;

View file

@ -1,39 +1,38 @@
using System.Collections.Frozen;
using Newtonsoft.Json;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Services;
namespace Penumbra.Api.Api;
public class PluginStateApi : IPenumbraApiPluginState, IApiService
public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public PluginStateApi(Configuration config, CommunicatorService communicator)
{
_config = config;
_communicator = communicator;
}
public string GetModDirectory()
=> _config.ModDirectory;
=> config.ModDirectory;
public string GetConfiguration()
=> JsonConvert.SerializeObject(_config, Formatting.Indented);
=> JsonConvert.SerializeObject(config, Formatting.Indented);
public event Action<string, bool>? ModDirectoryChanged
{
add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
remove => _communicator.ModDirectoryChanged.Unsubscribe(value!);
add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
}
public bool GetEnabledState()
=> _config.EnableMods;
=> config.EnableMods;
public event Action<bool>? EnabledChange
{
add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
remove => _communicator.EnabledChanged.Unsubscribe(value!);
add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
remove => communicator.EnabledChanged.Unsubscribe(value!);
}
public FrozenSet<string> SupportedFeatures
=> FeatureChecker.SupportedFeatures.ToFrozenSet();
public string[] CheckSupportedFeatures(IEnumerable<string> requiredFeatures)
=> requiredFeatures.Where(f => !FeatureChecker.Supported(f)).ToArray();
}

View file

@ -1,27 +1,57 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Interop;
using Penumbra.Interop.Services;
namespace Penumbra.Api.Api;
public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService
namespace Penumbra.Api.Api;
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
{
public void RedrawObject(int gameObjectIndex, RedrawType setting)
=> redrawService.RedrawObject(gameObjectIndex, setting);
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting));
}
public void RedrawObject(string name, RedrawType setting)
=> redrawService.RedrawObject(name, setting);
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting));
}
public void RedrawObject(IGameObject? gameObject, RedrawType setting)
=> redrawService.RedrawObject(gameObject, setting);
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting));
}
public void RedrawAll(RedrawType setting)
=> redrawService.RedrawAll(setting);
{
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
}
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
{
if (!collections.Storage.ById(collectionId, out var collection))
collection = ModCollection.Empty;
framework.RunOnFrameworkThread(() =>
{
foreach (var actor in objects.Objects)
{
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
if (collection == modCollection)
{
redrawService.RedrawObject(actor.ObjectIndex, setting);
}
}
});
}
public event GameObjectRedrawnDelegate? GameObjectRedrawn
{
add => redrawService.GameObjectRedrawn += value;
remove => redrawService.GameObjectRedrawn -= value;
}
}
}

View file

@ -1,8 +1,11 @@
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.String.Classes;
@ -13,10 +16,20 @@ public class TemporaryApi(
ObjectManager objects,
ActorManager actors,
CollectionManager collectionManager,
TempModManager tempMods) : IPenumbraApiTemporary, IApiService
TempModManager tempMods,
ApiHelpers apiHelpers,
ModManager modManager) : IPenumbraApiTemporary, IApiService
{
public Guid CreateTemporaryCollection(string name)
=> tempCollections.CreateTemporaryCollection(name);
public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
{
if (!IdentityChecker.Check(identity))
return (PenumbraApiEc.InvalidCredentials, Guid.Empty);
var collection = tempCollections.CreateTemporaryCollection(name);
if (collection == Guid.Empty)
return (PenumbraApiEc.UnknownError, collection);
return (PenumbraApiEc.Success, collection);
}
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
=> tempCollections.RemoveTemporaryCollection(collectionId)
@ -60,7 +73,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!MetaApi.ConvertManips(manipString, out var m))
if (!MetaApi.ConvertManips(manipString, out var m, out _))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
@ -86,7 +99,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!MetaApi.ConvertManips(manipString, out var m))
if (!MetaApi.ConvertManips(manipString, out var m, out _))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
@ -125,6 +138,177 @@ public class TemporaryApi(
return ApiHelpers.Return(ret, args);
}
public (PenumbraApiEc, (bool, bool, int, Dictionary<string, List<string>>)?, string) QueryTemporaryModSettings(Guid collectionId,
string modDirectory, string modName, int key)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName);
if (!collectionManager.Storage.ById(collectionId, out var collection))
return (ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args), null, string.Empty);
return QueryTemporaryModSettings(args, collection, modDirectory, modName, key);
}
public (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary<string, List<string>>)? Settings, string Source)
QueryTemporaryModSettingsPlayer(int objectIndex,
string modDirectory, string modName, int key)
{
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName);
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
return (ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args), null, string.Empty);
return QueryTemporaryModSettings(args, collection, modDirectory, modName, key);
}
private (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary<string, List<string>>)? Settings, string Source) QueryTemporaryModSettings(
in LazyString args, ModCollection collection, string modDirectory, string modName, int key)
{
if (!modManager.TryGetMod(modDirectory, modName, out var mod))
return (ApiHelpers.Return(PenumbraApiEc.ModMissing, args), null, string.Empty);
if (collection.Identity.Index <= 0)
return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty);
var settings = collection.GetTempSettings(mod.Index);
if (settings == null)
return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty);
if (settings.Lock > 0 && settings.Lock != key)
return (ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args), null, settings.Source);
return (ApiHelpers.Return(PenumbraApiEc.Success, args),
(settings.ForceInherit, settings.Enabled, settings.Priority.Value, settings.ConvertToShareable(mod).Settings), settings.Source);
}
public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled,
int priority,
IReadOnlyDictionary<string, IReadOnlyList<string>> options, string source, int key)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit,
"Enabled", enabled,
"Priority", priority, "Options", options, "Source", source, "Key", key);
if (!collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key);
}
public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled,
int priority,
IReadOnlyDictionary<string, IReadOnlyList<string>> options, string source, int key)
{
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled",
enabled,
"Priority", priority, "Options", options, "Source", source, "Key", key);
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key);
}
private PenumbraApiEc SetTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName,
bool inherit, bool enabled, int priority, IReadOnlyDictionary<string, IReadOnlyList<string>> options, string source, int key)
{
if (collection.Identity.Index <= 0)
return ApiHelpers.Return(PenumbraApiEc.TemporarySettingImpossible, args);
if (!modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key))
if (collection.GetTempSettings(mod.Index) is { Lock: > 0 } oldSettings && oldSettings.Lock != key)
return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args);
var newSettings = new TemporaryModSettings()
{
ForceInherit = inherit,
Enabled = enabled,
Priority = new ModPriority(priority),
Lock = key,
Source = source,
Settings = SettingList.Default(mod),
};
foreach (var (groupName, optionNames) in options)
{
var ec = ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIdx, out var setting);
if (ec != PenumbraApiEc.Success)
return ApiHelpers.Return(ec, args);
newSettings.Settings[groupIdx] = setting;
}
if (collectionManager.Editor.SetTemporarySettings(collection, mod, newSettings, key))
return ApiHelpers.Return(PenumbraApiEc.Success, args);
// This should not happen since all error cases had been checked before.
return ApiHelpers.Return(PenumbraApiEc.UnknownError, args);
}
public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key);
if (!collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key);
}
public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key)
{
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Key", key);
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key);
}
private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key)
{
if (collection.Identity.Index <= 0)
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
if (!modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
if (collection.GetTempSettings(mod.Index) is null)
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
if (!collectionManager.Editor.SetTemporarySettings(collection, mod, null, key))
return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args);
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key);
if (!collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
return RemoveAllTemporaryModSettings(args, collection, key);
}
public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key)
{
var args = ApiHelpers.Args("ObjectIndex", objectIndex, "Key", key);
if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
return RemoveAllTemporaryModSettings(args, collection, key);
}
private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key)
{
if (collection.Identity.Index <= 0)
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
var numRemoved = collectionManager.Editor.ClearTemporarySettings(collection, key);
return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args);
}
/// <summary>
/// Convert a dictionary of strings to a dictionary of game paths to full paths.
/// Only returns true if all paths can successfully be converted and added.

View file

@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
public void CloseMainWindow()
=> _configWindow.IsOpen = false;
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data)
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data)
{
if (ChangedItemClicked == null)
return;
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
var (type, id) = data.ToApiObject();
ChangedItemClicked.Invoke(button, type, id);
}
private void OnChangedItemHover(IIdentifiedObjectData? data)
private void OnChangedItemHover(IIdentifiedObjectData data)
{
if (ChangedItemTooltip == null)
return;
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
var (type, id) = data.ToApiObject();
ChangedItemTooltip.Invoke(type, id);
}
}

View file

@ -1,9 +1,11 @@
using Dalamud.Plugin.Services;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Mods.Settings;
namespace Penumbra.Api;
@ -12,23 +14,28 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller : WebApiController
{
// @formatter:off
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
[Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll();
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
// @formatter:on
}
public const string Prefix = "http://localhost:42069/";
private readonly IPenumbraApi _api;
private readonly IFramework _framework;
private WebServer? _server;
public HttpApi(Configuration config, IPenumbraApi api)
public HttpApi(Configuration config, IPenumbraApi api, IFramework framework)
{
_api = api;
_api = api;
_framework = framework;
if (config.EnableHttpApi)
CreateWebServer();
}
@ -44,7 +51,7 @@ public class HttpApi : IDisposable, IApiService
.WithUrlPrefix(Prefix)
.WithMode(HttpListenerMode.EmbedIO))
.WithCors(Prefix)
.WithWebApi("/api", m => m.WithController(() => new Controller(_api)));
.WithWebApi("/api", m => m.WithController(() => new Controller(_api, _framework)));
_server.StateChanged += (_, e) => Penumbra.Log.Information($"WebServer New State - {e.NewState}");
_server.RunAsync();
@ -59,60 +66,96 @@ public class HttpApi : IDisposable, IApiService
public void Dispose()
=> ShutdownWebServer();
private partial class Controller
private partial class Controller(IPenumbraApi api, IFramework framework)
{
private readonly IPenumbraApi _api;
public Controller(IPenumbraApi api)
=> _api = api;
public partial string GetModDirectory()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
return api.PluginState.GetModDirectory();
}
public partial object? GetMods()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
return _api.Mods.GetModList();
return api.Mods.GetModList();
}
public async partial Task Redraw()
{
var data = await HttpContext.GetRequestDataAsync<RedrawData>();
Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}.");
if (data.ObjectTableIndex >= 0)
_api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type);
else
_api.Redraw.RedrawAll(data.Type);
var data = await HttpContext.GetRequestDataAsync<RedrawData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] [{Environment.CurrentManagedThreadId}] {nameof(Redraw)} triggered with {data}.");
await framework.RunOnFrameworkThread(() =>
{
if (data.ObjectTableIndex >= 0)
api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type);
else
api.Redraw.RedrawAll(data.Type);
}).ConfigureAwait(false);
}
public partial void RedrawAll()
public async partial Task RedrawAll()
{
Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered.");
_api.Redraw.RedrawAll(RedrawType.Redraw);
await framework.RunOnFrameworkThread(() => { api.Redraw.RedrawAll(RedrawType.Redraw); }).ConfigureAwait(false);
}
public async partial Task ReloadMod()
{
var data = await HttpContext.GetRequestDataAsync<ModReloadData>();
var data = await HttpContext.GetRequestDataAsync<ModReloadData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(ReloadMod)} triggered with {data}.");
// Add the mod if it is not already loaded and if the directory name is given.
// AddMod returns Success if the mod is already loaded.
if (data.Path.Length != 0)
_api.Mods.AddMod(data.Path);
api.Mods.AddMod(data.Path);
// Reload the mod by path or name, which will also remove no-longer existing mods.
_api.Mods.ReloadMod(data.Path, data.Name);
api.Mods.ReloadMod(data.Path, data.Name);
}
public async partial Task InstallMod()
{
var data = await HttpContext.GetRequestDataAsync<ModInstallData>();
var data = await HttpContext.GetRequestDataAsync<ModInstallData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}.");
if (data.Path.Length != 0)
_api.Mods.InstallMod(data.Path);
api.Mods.InstallMod(data.Path);
}
public partial void OpenWindow()
{
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
_api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
}
public async partial Task FocusMod()
{
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered.");
if (data.Path.Length != 0)
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
}
public async partial Task SetModSettings()
{
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
await framework.RunOnFrameworkThread(() =>
{
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
if (data.Inherit.HasValue)
{
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
if (data.Inherit.Value)
return;
}
if (data.State.HasValue)
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
if (data.Priority.HasValue)
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
foreach (var (group, settings) in data.Settings ?? [])
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
}
).ConfigureAwait(false);
}
private record ModReloadData(string Path, string Name)
@ -122,6 +165,13 @@ public class HttpApi : IDisposable, IApiService
{ }
}
private record ModFocusData(string Path, string Name)
{
public ModFocusData()
: this(string.Empty, string.Empty)
{ }
}
private record ModInstallData(string Path)
{
public ModInstallData()
@ -135,5 +185,19 @@ public class HttpApi : IDisposable, IApiService
: this(string.Empty, RedrawType.Redraw, -1)
{ }
}
private record SetModSettingsData(
Guid? CollectionId,
string ModPath,
string ModName,
bool? Inherit,
bool? State,
int? Priority,
Dictionary<string, List<string>>? Settings)
{
public SetModSettingsData()
: this(null, string.Empty, string.Empty, null, null, null, null)
{}
}
}
}

View file

@ -0,0 +1,28 @@
using Dalamud.Plugin;
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.Api.Api;
using Serilog.Events;
namespace Penumbra.Api;
public sealed class IpcLaunchingProvider : IApiService
{
public IpcLaunchingProvider(IDalamudPluginInterface pi, Logger log)
{
try
{
using var subscriber = log.MainLogger.IsEnabled(LogEventLevel.Debug)
? IpcSubscribers.Launching.Subscriber(pi,
(major, minor) => log.Debug($"[IPC] Invoked Penumbra.Launching IPC with API Version {major}.{minor}."))
: null;
using var provider = IpcSubscribers.Launching.Provider(pi);
provider.Invoke(PenumbraApi.BreakingVersion, PenumbraApi.FeatureVersion);
}
catch (Exception ex)
{
log.Error($"[IPC] Could not invoke Penumbra.Launching IPC:\n{ex}");
}
}
}

View file

@ -2,6 +2,8 @@ using Dalamud.Plugin;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Helpers;
using Penumbra.Communication;
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
namespace Penumbra.Api;
@ -9,11 +11,13 @@ public sealed class IpcProviders : IDisposable, IApiService
{
private readonly List<IDisposable> _providers;
private readonly EventProvider _disposedProvider;
private readonly EventProvider _initializedProvider;
private readonly EventProvider _disposedProvider;
private readonly EventProvider _initializedProvider;
private readonly CharacterUtility _characterUtility;
public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api)
public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api, CharacterUtility characterUtility)
{
_characterUtility = characterUtility;
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
_providers =
@ -25,6 +29,7 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection),
IpcSubscribers.SetCollection.Provider(pi, api.Collection),
IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection),
IpcSubscribers.CheckCurrentChangedItemFunc.Provider(pi, api.Collection),
IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing),
IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing),
@ -35,6 +40,8 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState),
IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState),
IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState),
IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta),
IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta),
@ -47,12 +54,18 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
IpcSubscribers.ModMoved.Provider(pi, api.Mods),
IpcSubscribers.CreatingPcp.Provider(pi, api.Mods),
IpcSubscribers.ParsingPcp.Provider(pi, api.Mods),
IpcSubscribers.GetModPath.Provider(pi, api.Mods),
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItemAdapterDictionary.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItemAdapterList.Provider(pi, api.Mods),
IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings),
IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings),
@ -63,16 +76,19 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ApiVersion.Provider(pi, api),
new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility
new FuncProvider<int>(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility
new FuncProvider<int>(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility
IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState),
IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState),
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
@ -97,6 +113,14 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary),
IpcSubscribers.SetTemporaryModSettings.Provider(pi, api.Temporary),
IpcSubscribers.SetTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryModSettings.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary),
IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
IpcSubscribers.QueryTemporaryModSettings.Provider(pi, api.Temporary),
IpcSubscribers.QueryTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui),
IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui),
@ -107,11 +131,21 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
];
if (_characterUtility.Ready)
_initializedProvider.Invoke();
else
_characterUtility.LoadingFinished.Subscribe(OnCharacterUtilityReady, CharacterUtilityFinished.Priority.IpcProvider);
}
private void OnCharacterUtilityReady()
{
_initializedProvider.Invoke();
_characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady);
}
public void Dispose()
{
_characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady);
foreach (var provider in _providers)
provider.Dispose();
_providers.Clear();

View file

@ -1,7 +1,7 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
@ -121,6 +121,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
}).ToArray();
ImGui.OpenPopup("Changed Item List");
}
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
if (ImGui.Button("Redraw##ObjectCollection"))
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
}
private void DrawChangedItemPopup()

View file

@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;

View file

@ -1,6 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;

View file

@ -1,6 +1,6 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Services;
using Penumbra.Api.Api;

View file

@ -1,14 +1,20 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Api;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Api.IpcTester;
public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService
{
private int _gameObjectIndex;
private int _gameObjectIndex;
private string _metaBase64 = string.Empty;
private MetaDictionary _metaDict = new();
private byte _parsedVersion = byte.MaxValue;
public void Draw()
{
@ -17,6 +23,11 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService
return;
ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0);
if (ImUtf8.InputText("##metaText"u8, ref _metaBase64, "Base64 Metadata..."u8))
if (!MetaApi.ConvertManips(_metaBase64, out _metaDict!, out _parsedVersion))
_metaDict ??= new MetaDictionary();
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
@ -34,5 +45,8 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService
var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex);
ImGui.SetClipboardText(base64);
}
IpcTester.DrawIntro(string.Empty, "Parsed Data");
ImUtf8.Text($"Version: {_parsedVersion}, Count: {_metaDict.Count}");
}
}

View file

@ -1,8 +1,9 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
@ -22,16 +23,20 @@ public class ModSettingsIpcTester : IUiService, IDisposable
private bool _lastSettingChangeInherited;
private DateTimeOffset _lastSettingChange;
private string _settingsModDirectory = string.Empty;
private string _settingsModName = string.Empty;
private Guid? _settingsCollection;
private string _settingsCollectionName = string.Empty;
private bool _settingsIgnoreInheritance;
private bool _settingsInherit;
private bool _settingsEnabled;
private int _settingsPriority;
private IReadOnlyDictionary<string, (string[], GroupType)>? _availableSettings;
private Dictionary<string, List<string>>? _currentSettings;
private string _settingsModDirectory = string.Empty;
private string _settingsModName = string.Empty;
private Guid? _settingsCollection;
private string _settingsCollectionName = string.Empty;
private bool _settingsIgnoreInheritance;
private bool _settingsIgnoreTemporary;
private int _settingsKey;
private bool _settingsInherit;
private bool _settingsTemporary;
private bool _settingsEnabled;
private int _settingsPriority;
private IReadOnlyDictionary<string, (string[], GroupType)>? _availableSettings;
private Dictionary<string, List<string>>? _currentSettings;
private Dictionary<string, (bool, int, Dictionary<string, List<string>>, bool, bool)>? _allSettings;
public ModSettingsIpcTester(IDalamudPluginInterface pi)
{
@ -54,7 +59,9 @@ public class ModSettingsIpcTester : IUiService, IDisposable
ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100);
ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100);
ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName);
ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance);
ImUtf8.Checkbox("Ignore Inheritance"u8, ref _settingsIgnoreInheritance);
ImUtf8.Checkbox("Ignore Temporary"u8, ref _settingsIgnoreTemporary);
ImUtf8.InputScalar("Key"u8, ref _settingsKey);
var collection = _settingsCollection.GetValueOrDefault(Guid.Empty);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
@ -83,10 +90,11 @@ public class ModSettingsIpcTester : IUiService, IDisposable
_lastSettingsError = ret.Item1;
if (ret.Item1 == PenumbraApiEc.Success)
{
_settingsEnabled = ret.Item2?.Item1 ?? false;
_settingsInherit = ret.Item2?.Item4 ?? true;
_settingsPriority = ret.Item2?.Item2 ?? 0;
_currentSettings = ret.Item2?.Item3;
_settingsEnabled = ret.Item2?.Item1 ?? false;
_settingsInherit = ret.Item2?.Item4 ?? true;
_settingsTemporary = false;
_settingsPriority = ret.Item2?.Item2 ?? 0;
_currentSettings = ret.Item2?.Item3;
}
else
{
@ -94,6 +102,40 @@ public class ModSettingsIpcTester : IUiService, IDisposable
}
}
IpcTester.DrawIntro(GetCurrentModSettingsWithTemp.Label, "Get Current Settings With Temp");
if (ImGui.Button("Get##CurrentTemp"))
{
var ret = new GetCurrentModSettingsWithTemp(_pi)
.Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey);
_lastSettingsError = ret.Item1;
if (ret.Item1 == PenumbraApiEc.Success)
{
_settingsEnabled = ret.Item2?.Item1 ?? false;
_settingsInherit = ret.Item2?.Item4 ?? true;
_settingsTemporary = ret.Item2?.Item5 ?? false;
_settingsPriority = ret.Item2?.Item2 ?? 0;
_currentSettings = ret.Item2?.Item3;
}
else
{
_currentSettings = null;
}
}
IpcTester.DrawIntro(GetAllModSettings.Label, "Get All Mod Settings");
if (ImGui.Button("Get##All"))
{
var ret = new GetAllModSettings(_pi).Invoke(collection, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey);
_lastSettingsError = ret.Item1;
_allSettings = ret.Item2;
}
if (_allSettings != null)
{
ImGui.SameLine();
ImUtf8.Text($"{_allSettings.Count} Mods");
}
IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod");
ImGui.Checkbox("##inherit", ref _settingsInherit);
ImGui.SameLine();

View file

@ -1,6 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;

View file

@ -1,10 +1,11 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
@ -12,7 +13,7 @@ namespace Penumbra.Api.IpcTester;
public class PluginStateIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
public readonly EventSubscriber Initialized;
public readonly EventSubscriber Disposed;
@ -26,6 +27,9 @@ public class PluginStateIpcTester : IUiService, IDisposable
private readonly List<DateTimeOffset> _initializedList = [];
private readonly List<DateTimeOffset> _disposedList = [];
private string _requiredFeatureString = string.Empty;
private string[] _requiredFeatures = [];
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
private bool? _lastEnabledValue;
@ -48,12 +52,15 @@ public class PluginStateIpcTester : IUiService, IDisposable
EnabledChange.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Plugin State");
if (!_)
return;
if (ImUtf8.InputText("Required Features"u8, ref _requiredFeatureString))
_requiredFeatures = _requiredFeatureString.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
@ -71,6 +78,12 @@ public class PluginStateIpcTester : IUiService, IDisposable
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
IpcTester.DrawIntro(SupportedFeatures.Label, "Supported Features");
ImUtf8.Text(string.Join(", ", new SupportedFeatures(_pi).Invoke()));
IpcTester.DrawIntro(CheckSupportedFeatures.Label, "Missing Features");
ImUtf8.Text(string.Join(", ", new CheckSupportedFeatures(_pi).Invoke(_requiredFeatures)));
DrawConfigPopup();
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
if (ImGui.Button("Get"))

View file

@ -1,6 +1,6 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;

View file

@ -1,5 +1,5 @@
using Dalamud.Plugin;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.IpcSubscribers;

View file

@ -1,9 +1,10 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;

View file

@ -1,7 +1,8 @@
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
@ -33,9 +34,11 @@ public class TemporaryIpcTester(
private string _tempCollectionName = string.Empty;
private string _tempCollectionGuidName = string.Empty;
private string _tempModName = string.Empty;
private string _modDirectory = string.Empty;
private string _tempGamePath = "test/game/path.mtrl";
private string _tempFilePath = "test/success.mtrl";
private string _tempManipulation = string.Empty;
private string _identity = string.Empty;
private PenumbraApiEc _lastTempError;
private int _tempActorIndex;
private bool _forceOverwrite;
@ -46,10 +49,12 @@ public class TemporaryIpcTester(
if (!_)
return;
ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128);
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256);
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
@ -70,7 +75,7 @@ public class TemporaryIpcTester(
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
if (ImGui.Button("Create##Collection"))
{
LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName);
_lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
if (_tempGuid == null)
{
_tempGuid = LastCreatedCollectionId;
@ -121,6 +126,115 @@ public class TemporaryIpcTester(
IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections");
if (ImGui.Button("Remove##ModAll"))
_lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue);
IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection");
if (ImUtf8.Button("Set##SetTemporary"u8))
_lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337,
new Dictionary<string, IReadOnlyList<string>>(),
"IPC Tester", 1337);
IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection");
if (ImUtf8.Button("Set##SetTemporaryPlayer"u8))
_lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337,
new Dictionary<string, IReadOnlyList<string>>(),
"IPC Tester", 1337);
IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection");
if (ImUtf8.Button("Remove##RemoveTemporary"u8))
_lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1337);
ImGui.SameLine();
if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8))
_lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1338);
IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection");
if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8))
_lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1337);
ImGui.SameLine();
if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8))
_lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1338);
IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection");
if (ImUtf8.Button("Remove##RemoveAllTemporary"u8))
_lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1337);
ImGui.SameLine();
if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporary"u8))
_lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1338);
IpcTester.DrawIntro(RemoveAllTemporaryModSettingsPlayer.Label, "Remove All Temporary Mod Settings from game object Collection");
if (ImUtf8.Button("Remove##RemoveAllTemporaryPlayer"u8))
_lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1337);
ImGui.SameLine();
if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8))
_lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338);
IpcTester.DrawIntro(QueryTemporaryModSettings.Label, "Query Temporary Mod Settings from specific Collection");
ImUtf8.Button("Query##QueryTemporaryModSettings"u8);
if (ImGui.IsItemHovered())
{
_lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1337);
DrawTooltip(settings, source);
}
ImGui.SameLine();
ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporary"u8);
if (ImGui.IsItemHovered())
{
_lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1338);
DrawTooltip(settings, source);
}
IpcTester.DrawIntro(QueryTemporaryModSettingsPlayer.Label, "Query Temporary Mod Settings from game object Collection");
ImUtf8.Button("Query##QueryTemporaryModSettingsPlayer"u8);
if (ImGui.IsItemHovered())
{
_lastTempError =
new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1337);
DrawTooltip(settings, source);
}
ImGui.SameLine();
ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporaryPlayer"u8);
if (ImGui.IsItemHovered())
{
_lastTempError =
new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1338);
DrawTooltip(settings, source);
}
void DrawTooltip((bool ForceInherit, bool Enabled, int Priority, Dictionary<string, List<string>> Settings)? settings, string source)
{
using var tt = ImUtf8.Tooltip();
ImUtf8.Text($"Query returned {_lastTempError}");
if (settings != null)
ImUtf8.Text($"Settings created by {(source.Length == 0 ? "Unknown Source" : source)}:");
else
ImUtf8.Text(source.Length > 0 ? $"Locked by {source}." : "No settings exist.");
ImGui.Separator();
if (settings == null)
{
return;
}
using (ImUtf8.Group())
{
ImUtf8.Text("Force Inherit"u8);
ImUtf8.Text("Enabled"u8);
ImUtf8.Text("Priority"u8);
foreach (var group in settings.Value.Settings.Keys)
ImUtf8.Text(group);
}
ImGui.SameLine();
using (ImUtf8.Group())
{
ImUtf8.Text($"{settings.Value.ForceInherit}");
ImUtf8.Text($"{settings.Value.Enabled}");
ImUtf8.Text($"{settings.Value.Priority}");
foreach (var group in settings.Value.Settings.Values)
ImUtf8.Text(string.Join("; ", group));
}
}
}
public void DrawCollections()
@ -146,10 +260,10 @@ public class TemporaryIpcTester(
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(collection.Identifier);
ImGuiUtil.CopyOnClickSelectable(collection.Identity.Identifier);
}
ImGuiUtil.DrawTableColumn(collection.Name);
ImGuiUtil.DrawTableColumn(collection.Identity.Name);
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
ImGuiUtil.DrawTableColumn(string.Join(", ",
@ -170,7 +284,7 @@ public class TemporaryIpcTester(
foreach (var mod in list)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Name);
ImGui.TextUnformatted(mod.Name.Text);
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Priority.ToString());
ImGui.TableNextColumn();
@ -199,7 +313,7 @@ public class TemporaryIpcTester(
{
PrintList("All", tempMods.ModsForAllCollections);
foreach (var (collection, list) in tempMods.Mods)
PrintList(collection.Name, list);
PrintList(collection.Identity.Name, list);
}
}
}

View file

@ -1,5 +1,5 @@
using Dalamud.Plugin;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;

View file

@ -0,0 +1,103 @@
using Penumbra.GameData.Data;
using Penumbra.Mods.Manager;
namespace Penumbra.Api;
public sealed class ModChangedItemAdapter(WeakReference<ModStorage> storage)
: IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>,
IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>
{
IEnumerator<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>
IEnumerable<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>.GetEnumerator()
=> Storage.Select(m => (m.Identifier, (IReadOnlyDictionary<string, object?>)new ChangedItemDictionaryAdapter(m.ChangedItems)))
.GetEnumerator();
public IEnumerator<KeyValuePair<string, IReadOnlyDictionary<string, object?>>> GetEnumerator()
=> Storage.Select(m => new KeyValuePair<string, IReadOnlyDictionary<string, object?>>(m.Identifier,
new ChangedItemDictionaryAdapter(m.ChangedItems)))
.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> Storage.Count;
public bool ContainsKey(string key)
=> Storage.TryGetMod(key, string.Empty, out _);
public bool TryGetValue(string key, [NotNullWhen(true)] out IReadOnlyDictionary<string, object?>? value)
{
if (Storage.TryGetMod(key, string.Empty, out var mod))
{
value = new ChangedItemDictionaryAdapter(mod.ChangedItems);
return true;
}
value = null;
return false;
}
public IReadOnlyDictionary<string, object?> this[string key]
=> TryGetValue(key, out var v) ? v : throw new KeyNotFoundException();
(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)
IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>.this[int index]
{
get
{
var m = Storage[index];
return (m.Identifier, new ChangedItemDictionaryAdapter(m.ChangedItems));
}
}
public IEnumerable<string> Keys
=> Storage.Select(m => m.Identifier);
public IEnumerable<IReadOnlyDictionary<string, object?>> Values
=> Storage.Select(m => new ChangedItemDictionaryAdapter(m.ChangedItems));
private ModStorage Storage
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
get => storage.TryGetTarget(out var t)
? t
: throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed.");
}
private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData> data) : IReadOnlyDictionary<string, object?>
{
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
=> data.Select(d => new KeyValuePair<string, object?>(d.Key, d.Value?.ToInternalObject())).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> data.Count;
public bool ContainsKey(string key)
=> data.ContainsKey(key);
public bool TryGetValue(string key, out object? value)
{
if (data.TryGetValue(key, out var v))
{
value = v?.ToInternalObject();
return true;
}
value = null;
return false;
}
public object? this[string key]
=> data[key]?.ToInternalObject();
public IEnumerable<string> Keys
=> data.Keys;
public IEnumerable<object?> Values
=> data.Values.Select(v => v?.ToInternalObject());
}
}

View file

@ -85,13 +85,13 @@ public class TempModManager : IDisposable, IService
{
if (removed)
{
Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}.");
Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.Identity.AnonymizedName}.");
collection.Remove(mod);
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false);
}
else
{
Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}.");
Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.Identity.AnonymizedName}.");
collection.Apply(mod, created);
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false);
}

View file

@ -0,0 +1,57 @@
using Dalamud.Bindings.ImGui;
using OtterGui.Text;
namespace Penumbra;
public enum ChangedItemMode
{
GroupedCollapsed,
GroupedExpanded,
Alphabetical,
}
public static class ChangedItemModeExtensions
{
public static ReadOnlySpan<byte> ToName(this ChangedItemMode mode)
=> mode switch
{
ChangedItemMode.GroupedCollapsed => "Grouped (Collapsed)"u8,
ChangedItemMode.GroupedExpanded => "Grouped (Expanded)"u8,
ChangedItemMode.Alphabetical => "Alphabetical"u8,
_ => "Error"u8,
};
public static ReadOnlySpan<byte> ToTooltip(this ChangedItemMode mode)
=> mode switch
{
ChangedItemMode.GroupedCollapsed =>
"Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item."u8,
ChangedItemMode.GroupedExpanded =>
"Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item."u8,
ChangedItemMode.Alphabetical => "Display all changed items in a single list sorted alphabetically."u8,
_ => ""u8,
};
public static bool DrawCombo(ReadOnlySpan<byte> label, ChangedItemMode value, float width, Action<ChangedItemMode> setter)
{
ImGui.SetNextItemWidth(width);
using var combo = ImUtf8.Combo(label, value.ToName());
if (!combo)
return false;
var ret = false;
foreach (var newValue in Enum.GetValues<ChangedItemMode>())
{
var selected = ImUtf8.Selectable(newValue.ToName(), newValue == value);
if (selected)
{
ret = true;
setter(newValue);
}
ImUtf8.HoverTooltip(newValue.ToTooltip());
}
return ret;
}
}

View file

@ -0,0 +1,121 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class AtchCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtchIdentifier, AtchEntry>(manager, collection)
{
private readonly Dictionary<GenderRace, (AtchFile, HashSet<AtchIdentifier>)> _atchFiles = [];
public bool HasFile(GenderRace gr)
=> _atchFiles.ContainsKey(gr);
public bool GetFile(GenderRace gr, [NotNullWhen(true)] out AtchFile? file)
{
if (!_atchFiles.TryGetValue(gr, out var p))
{
file = null;
return false;
}
file = p.Item1;
return true;
}
public void Reset()
{
foreach (var (_, (_, set)) in _atchFiles)
set.Clear();
_atchFiles.Clear();
Clear();
}
protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry)
{
Collection.Counters.IncrementAtch();
ApplyFile(identifier, entry);
}
private void ApplyFile(AtchIdentifier identifier, AtchEntry entry)
{
try
{
if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair))
{
if (!Manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile))
throw new Exception($"Invalid Atch File for {identifier.GenderRace.ToName()} requested.");
pair = (baseFile.Clone(), []);
}
if (!Apply(pair.Item1, identifier, entry))
return;
pair.Item2.Add(identifier);
_atchFiles[identifier.GenderRace] = pair;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not apply ATCH Manipulation {identifier}:\n{e}");
}
}
protected override void RevertModInternal(AtchIdentifier identifier)
{
Collection.Counters.IncrementAtch();
if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair))
return;
if (!pair.Item2.Remove(identifier))
return;
if (pair.Item2.Count == 0)
{
_atchFiles.Remove(identifier.GenderRace);
return;
}
var def = GetDefault(Manager, identifier);
if (def == null)
throw new Exception($"Reverting an .atch mod had no default value for the identifier to revert to.");
Apply(pair.Item1, identifier, def.Value);
}
public static AtchEntry? GetDefault(MetaFileManager manager, AtchIdentifier identifier)
{
if (!manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile))
return null;
if (baseFile.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point)
return null;
if (point.Entries.Length <= identifier.EntryIndex)
return null;
return point.Entries[identifier.EntryIndex];
}
public static bool Apply(AtchFile file, AtchIdentifier identifier, in AtchEntry entry)
{
if (file.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point)
return false;
if (point.Entries.Length <= identifier.EntryIndex)
return false;
point.Entries[identifier.EntryIndex] = entry;
return true;
}
protected override void Dispose(bool _)
{
Clear();
_atchFiles.Clear();
}
}

View file

@ -0,0 +1,65 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtrIdentifier, AtrEntry>(manager, collection)
{
public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace)
=> DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.CheckEntry(slot, id, genderRace) is false;
public int EnabledCount { get; private set; }
public int DisabledCount { get; private set; }
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> Data
=> _atrData;
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _atrData = [];
public void Reset()
{
Clear();
_atrData.Clear();
DisabledCount = 0;
EnabledCount = 0;
}
protected override void Dispose(bool _)
=> Reset();
protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry)
{
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
{
value = [];
_atrData.Add(identifier.Attribute, value);
}
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _))
{
if (entry.Value)
++EnabledCount;
else
++DisabledCount;
}
}
protected override void RevertModInternal(AtrIdentifier identifier)
{
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
return;
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which))
{
if (which)
--EnabledCount;
else
--DisabledCount;
if (value.IsEmpty)
_atrData.Remove(identifier.Attribute);
}
}
}

View file

@ -1,4 +1,4 @@
using OtterGui;
using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
@ -7,6 +7,7 @@ using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.Util;
using Penumbra.GameData.Data;
using OtterGui.Extensions;
namespace Penumbra.Collections.Cache;
@ -22,7 +23,7 @@ public sealed class CollectionCache : IDisposable
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> _changedItems = [];
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta;
@ -31,7 +32,7 @@ public sealed class CollectionCache : IDisposable
public int Calculating = -1;
public string AnonymizedName
=> _collection.AnonymizedName;
=> _collection.Identity.AnonymizedName;
public IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> ConflictDict.Values;
@ -42,7 +43,7 @@ public sealed class CollectionCache : IDisposable
private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
{
get
{
@ -177,7 +178,7 @@ public sealed class CollectionCache : IDisposable
var (paths, manipulations) = ModData.RemoveMod(mod);
if (addMetaChanges)
_collection.IncrementCounter();
_collection.Counters.IncrementChange();
foreach (var path in paths)
{
@ -228,24 +229,33 @@ public sealed class CollectionCache : IDisposable
foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod);
foreach (var (identifier, entry) in files.Manipulations.Eqp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Eqdp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Est)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Gmp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Rsp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Imc)
AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!);
if (files.Manipulations.Count > 0)
{
foreach (var (identifier, entry) in files.Manipulations.Eqp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Eqdp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Est)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Gmp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Rsp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Imc)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Atch)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Shp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Atr)
AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!);
}
if (addMetaChanges)
{
_collection.IncrementCounter();
_collection.Counters.IncrementChange();
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
@ -255,7 +265,7 @@ public sealed class CollectionCache : IDisposable
if (mod.Index < 0)
return mod.GetData();
var settings = _collection[mod.Index].Settings;
var settings = _collection.GetActualSettings(mod.Index).Settings;
return settings is not { Enabled: true }
? AppliedModData.Empty
: mod.GetData(settings);
@ -269,6 +279,24 @@ public sealed class CollectionCache : IDisposable
_manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod);
}
private static bool IsRedirectionSupported(Utf8GamePath path, IMod mod)
{
var ext = path.Extension().AsciiToLower().ToString();
switch (ext)
{
case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc":
Penumbra.Messager.NotificationMessage(
$"Redirection of {ext} files for {mod.Name} is unsupported. This probably means that the mod is outdated and may not work correctly.\n\nPlease tell the mod creator to use the corresponding meta manipulations instead.",
NotificationType.Warning);
return false;
case ".lvb" or ".lgb" or ".sgb":
Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.\n\nThis mod will probably not work correctly.",
NotificationType.Warning);
return false;
default: return true;
}
}
// Add a specific file redirection, handling potential conflicts.
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
@ -278,6 +306,9 @@ public sealed class CollectionCache : IDisposable
if (!CheckFullPath(path, file))
return;
if (!IsRedirectionSupported(path, mod))
return;
try
{
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
@ -337,8 +368,9 @@ public sealed class CollectionCache : IDisposable
// Returns if the added mod takes priority before the existing mod.
private bool AddConflict(object data, IMod addedMod, IMod existingMod)
{
var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority;
var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority;
var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority;
var existingPriority =
existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority;
if (existingPriority < addedPriority)
{
@ -403,17 +435,17 @@ public sealed class CollectionCache : IDisposable
// Identify and record all manipulated objects for this entire collection.
private void SetChangedItems()
{
if (_changedItemsSaveCounter == _collection.ChangeCounter)
if (_changedItemsSaveCounter == _collection.Counters.Change)
return;
try
{
_changedItemsSaveCounter = _collection.ChangeCounter;
_changedItemsSaveCounter = _collection.Counters.Change;
_changedItems.Clear();
// Skip IMCs because they would result in far too many false-positive items,
// since they are per set instead of per item-slot/item/variant.
var identifier = _manager.MetaFileManager.Identifier;
var items = new SortedList<string, IIdentifiedObjectData?>(512);
var items = new SortedList<string, IIdentifiedObjectData>(512);
void AddItems(IMod mod)
{
@ -422,7 +454,8 @@ public sealed class CollectionCache : IDisposable
if (!_changedItems.TryGetValue(name, out var data))
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
else if (!data.Item1.Contains(mod))
_changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
_changedItems[name] = (data.Item1.Append(mod),
obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y)
_changedItems[name] = (data.Item1, x + y);
}

View file

@ -71,7 +71,7 @@ public class CollectionCacheManager : IDisposable, IService
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager);
if (!MetaFileManager.CharacterUtility.Ready)
MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters;
MetaFileManager.CharacterUtility.LoadingFinished.Subscribe(IncrementCounters, CharacterUtilityFinished.Priority.CollectionCacheManager);
}
public void Dispose()
@ -83,7 +83,7 @@ public class CollectionCacheManager : IDisposable, IService
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange);
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
foreach (var collection in _storage)
{
@ -114,16 +114,16 @@ public class CollectionCacheManager : IDisposable, IService
/// <summary> Only creates a new cache, does not update an existing one. </summary>
public bool CreateCache(ModCollection collection)
{
if (collection.Index == ModCollection.Empty.Index)
if (collection.Identity.Index == ModCollection.Empty.Identity.Index)
return false;
if (collection._cache != null)
return false;
collection._cache = new CollectionCache(this, collection);
if (collection.Index > 0)
if (collection.Identity.Index > 0)
Interlocked.Increment(ref _count);
Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}.");
Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}.");
return true;
}
@ -132,32 +132,32 @@ public class CollectionCacheManager : IDisposable, IService
/// Does not create caches.
/// </summary>
public void CalculateEffectiveFileList(ModCollection collection)
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier,
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identity.Identifier,
() => CalculateEffectiveFileListInternal(collection));
private void CalculateEffectiveFileListInternal(ModCollection collection)
{
// Skip the empty collection.
if (collection.Index == 0)
if (collection.Identity.Index == 0)
return;
Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}");
Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName}");
if (!collection.HasCache)
{
Penumbra.Log.Error(
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists.");
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists.");
}
else if (collection._cache!.Calculating != -1)
{
Penumbra.Log.Error(
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
}
else
{
FullRecalculation(collection);
Penumbra.Log.Debug(
$"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished.");
$"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.Identity.AnonymizedName} finished.");
}
}
@ -171,8 +171,7 @@ public class CollectionCacheManager : IDisposable, IService
try
{
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty,
FullPath.Empty,
null);
FullPath.Empty, null);
cache.ResolvedFiles.Clear();
cache.Meta.Reset();
cache.ConflictDict.Clear();
@ -187,7 +186,7 @@ public class CollectionCacheManager : IDisposable, IService
foreach (var mod in _modStorage)
cache.AddModSync(mod, false);
collection.IncrementCounter();
collection.Counters.IncrementChange();
MetaFileManager.ApplyDefaultFiles(collection);
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty,
@ -213,7 +212,7 @@ public class CollectionCacheManager : IDisposable, IService
else
{
RemoveCache(old);
if (type is not CollectionType.Inactive && newCollection != null && newCollection.Index != 0 && CreateCache(newCollection))
if (type is not CollectionType.Inactive && newCollection != null && newCollection.Identity.Index != 0 && CreateCache(newCollection))
CalculateEffectiveFileList(newCollection);
if (type is CollectionType.Default)
@ -231,11 +230,11 @@ public class CollectionCacheManager : IDisposable, IService
{
case ModPathChangeType.Deleted:
case ModPathChangeType.StartingReload:
foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
collection._cache!.RemoveMod(mod, true);
break;
case ModPathChangeType.Moved:
foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
collection._cache!.ReloadMod(mod, true);
break;
}
@ -246,7 +245,7 @@ public class CollectionCacheManager : IDisposable, IService
if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded))
return;
foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
collection._cache!.AddMod(mod, true);
}
@ -258,12 +257,12 @@ public class CollectionCacheManager : IDisposable, IService
private void RemoveCache(ModCollection? collection)
{
if (collection != null
&& collection.Index > ModCollection.Empty.Index
&& collection.Index != _active.Default.Index
&& collection.Index != _active.Interface.Index
&& collection.Index != _active.Current.Index
&& _active.SpecialAssignments.All(c => c.Value.Index != collection.Index)
&& _active.Individuals.All(c => c.Collection.Index != collection.Index))
&& collection.Identity.Index > ModCollection.Empty.Identity.Index
&& collection.Identity.Index != _active.Default.Identity.Index
&& collection.Identity.Index != _active.Interface.Identity.Index
&& collection.Identity.Index != _active.Current.Identity.Index
&& _active.SpecialAssignments.All(c => c.Value.Identity.Index != collection.Identity.Index)
&& _active.Individuals.All(c => c.Collection.Identity.Index != collection.Identity.Index))
ClearCache(collection);
}
@ -273,7 +272,7 @@ public class CollectionCacheManager : IDisposable, IService
{
if (type is ModOptionChangeType.PrepareChange)
{
foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true }))
foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
collection._cache!.RemoveMod(mod, false);
return;
@ -284,7 +283,7 @@ public class CollectionCacheManager : IDisposable, IService
if (!recomputeList)
return;
foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true }))
foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
{
if (justAdd)
collection._cache!.AddMod(mod, true);
@ -297,8 +296,8 @@ public class CollectionCacheManager : IDisposable, IService
private void IncrementCounters()
{
foreach (var collection in _storage.Where(c => c.HasCache))
collection.IncrementCounter();
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
collection.Counters.IncrementChange();
MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _)
@ -317,7 +316,7 @@ public class CollectionCacheManager : IDisposable, IService
cache.AddMod(mod!, true);
else if (oldValue == Setting.True)
cache.RemoveMod(mod!, true);
else if (collection[mod!.Index].Settings?.Enabled == true)
else if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
cache.ReloadMod(mod!, true);
else
cache.RemoveMod(mod!, true);
@ -329,10 +328,13 @@ public class CollectionCacheManager : IDisposable, IService
break;
case ModSettingChange.Setting:
if (collection[mod!.Index].Settings?.Enabled == true)
cache.ReloadMod(mod!, true);
if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
cache.ReloadMod(mod, true);
break;
case ModSettingChange.TemporarySetting:
cache.ReloadMod(mod!, true);
break;
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
FullRecalculation(collection);
@ -359,9 +361,9 @@ public class CollectionCacheManager : IDisposable, IService
collection._cache!.Dispose();
collection._cache = null;
if (collection.Index > 0)
if (collection.Identity.Index > 0)
Interlocked.Decrement(ref _count);
Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}.");
Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}.");
}
/// <summary>

View file

@ -48,8 +48,8 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection)
? entry.HasFlag(EqpEntry.BodyHideGlovesL)
: entry.HasFlag(EqpEntry.BodyHideGlovesM);
return testFlag
? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS
: entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS);
? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS
: entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]

View file

@ -15,6 +15,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
private bool _doNotHideVieraHats;
private bool _doNotHideHrothgarHats;
private bool _hideAuRaHorns;
private bool _hideVieraEars;
private bool _hideMiqoteEars;
public new void Clear()
{
@ -26,6 +29,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
_doNotHideRingR.Clear();
_doNotHideHrothgarHats = false;
_doNotHideVieraHats = false;
_hideAuRaHorns = false;
_hideVieraEars = false;
_hideMiqoteEars = false;
}
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
@ -39,8 +45,20 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
if (_doNotHideHrothgarHats)
original |= EqpEntry.HeadShowHrothgarHat;
if (_hideAuRaHorns)
original &= ~EqpEntry.HeadShowEarAuRa;
if (_hideVieraEars)
original &= ~EqpEntry.HeadShowEarViera;
if (_hideMiqoteEars)
original &= ~EqpEntry.HeadShowEarMiqote;
if (_doNotHideEarrings.Contains(armor[5].Set))
original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura;
original |= EqpEntry.HeadShowEarringsHyurRoe
| EqpEntry.HeadShowEarringsLalaElezen
| EqpEntry.HeadShowEarringsMiqoHrothViera
| EqpEntry.HeadShowEarringsAura;
if (_doNotHideNecklace.Contains(armor[6].Set))
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
@ -53,6 +71,7 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
if (_doNotHideRingL.Contains(armor[9].Set))
original |= EqpEntry.HandShowRingL;
return original;
}
@ -71,6 +90,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true),
GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true),
GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true),
_ => false,
};
return true;
@ -90,6 +112,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false),
GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false),
GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false),
_ => false,
};
return true;

View file

@ -39,7 +39,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection)
protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry)
{
++Collection.ImcChangeCounter;
Collection.Counters.IncrementImc();
ApplyFile(identifier, entry);
}
@ -51,7 +51,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection)
if (!_imcFiles.TryGetValue(path, out var pair))
pair = (new ImcFile(Manager, identifier), []);
if (!Apply(pair.Item1, identifier, entry))
return;
@ -71,7 +70,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection)
protected override void RevertModInternal(ImcIdentifier identifier)
{
++Collection.ImcChangeCounter;
Collection.Counters.IncrementImc();
var path = identifier.GamePath().Path;
if (!_imcFiles.TryGetValue(path, out var pair))
return;

View file

@ -1,4 +1,5 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
@ -14,11 +15,14 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
public readonly GmpCache Gmp = new(manager, collection);
public readonly RspCache Rsp = new(manager, collection);
public readonly ImcCache Imc = new(manager, collection);
public readonly AtchCache Atch = new(manager, collection);
public readonly ShpCache Shp = new(manager, collection);
public readonly AtrCache Atr = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new();
public bool IsDisposed { get; private set; }
public int Count
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count;
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
@ -27,6 +31,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
.Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void Reset()
@ -37,6 +44,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Gmp.Reset();
Rsp.Reset();
Imc.Reset();
Atch.Reset();
Shp.Reset();
Atr.Reset();
GlobalEqp.Clear();
}
@ -52,6 +62,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Gmp.Dispose();
Rsp.Dispose();
Imc.Dispose();
Atch.Dispose();
Shp.Dispose();
Atr.Dispose();
}
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
@ -65,6 +78,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod),
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod),
AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod),
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
_ => false,
};
@ -85,6 +101,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
GmpIdentifier i => Gmp.RevertMod(i, out mod),
ImcIdentifier i => Imc.RevertMod(i, out mod),
RspIdentifier i => Rsp.RevertMod(i, out mod),
AtchIdentifier i => Atch.RevertMod(i, out mod),
ShpIdentifier i => Shp.RevertMod(i, out mod),
AtrIdentifier i => Atr.RevertMod(i, out mod),
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
_ => (mod = null) != null,
};
@ -100,6 +119,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e),
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e),
AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e),
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
_ => false,
};

View file

@ -0,0 +1,181 @@
using System.Collections.Frozen;
using OtterGui.Extensions;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
namespace Penumbra.Collections.Cache;
public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryId Id), ulong>
{
public static readonly IReadOnlyList<GenderRace> GenderRaceValues =
[
GenderRace.Unknown, GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale,
GenderRace.ElezenMale, GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale,
GenderRace.RoegadynFemale, GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale,
GenderRace.HrothgarMale, GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale,
];
public static readonly FrozenDictionary<GenderRace, int> GenderRaceIndices =
GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index);
private readonly BitArray _allIds = new(2 * (ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count);
public bool? this[HumanSlot slot]
=> AllCheck(ToIndex(slot, 0));
public bool? this[GenderRace genderRace]
=> ToIndex(HumanSlot.Unknown, genderRace, out var index) ? AllCheck(index) : null;
public bool? this[HumanSlot slot, GenderRace genderRace]
=> ToIndex(slot, genderRace, out var index) ? AllCheck(index) : null;
public bool? All
=> Convert(_allIds[2 * AllIndex], _allIds[2 * AllIndex + 1]);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private bool? AllCheck(int idx)
=> Convert(_allIds[idx], _allIds[idx + 1]);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static int ToIndex(HumanSlot slot, int genderRaceIndex)
=> 2 * (slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count);
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public bool? CheckEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace)
{
if (!GenderRaceIndices.TryGetValue(genderRace, out var index))
return null;
// Check for specific ID.
if (TryGetValue((slot, id), out var flags))
{
// Check completely specified entry.
if (Convert(flags, 2 * index) is { } specified)
return specified;
// Check any gender / race.
if (Convert(flags, 0) is { } anyGr)
return anyGr;
}
// Check for specified gender / race and slot, but no ID.
if (AllCheck(ToIndex(slot, index)) is { } noIdButGr)
return noIdButGr;
// Check for specified gender / race but no slot or ID.
if (AllCheck(ToIndex(HumanSlot.Unknown, index)) is { } noSlotButGr)
return noSlotButGr;
// Check for specified slot but no gender / race or ID.
if (AllCheck(ToIndex(slot, 0)) is { } noGrButSlot)
return noGrButSlot;
return All;
}
public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool? value, out bool which)
{
which = false;
if (!GenderRaceIndices.TryGetValue(genderRace, out var index))
return false;
if (!id.HasValue)
{
var slotIndex = ToIndex(slot, index);
var ret = false;
if (value is true)
{
if (!_allIds[slotIndex])
ret = true;
_allIds[slotIndex] = true;
_allIds[slotIndex + 1] = false;
}
else if (value is false)
{
if (!_allIds[slotIndex + 1])
ret = true;
_allIds[slotIndex] = false;
_allIds[slotIndex + 1] = true;
}
else
{
if (_allIds[slotIndex])
{
which = true;
ret = true;
}
else if (_allIds[slotIndex + 1])
{
which = false;
ret = true;
}
_allIds[slotIndex] = false;
_allIds[slotIndex + 1] = false;
}
return ret;
}
if (TryGetValue((slot, id.Value), out var flags))
{
index *= 2;
var newFlags = value switch
{
true => (flags | (1ul << index)) & ~(1ul << (index + 1)),
false => (flags & ~(1ul << index)) | (1ul << (index + 1)),
_ => flags & ~(1ul << index) & ~(1ul << (index + 1)),
};
if (newFlags == flags)
return false;
this[(slot, id.Value)] = newFlags;
which = (flags & (1ul << index)) is not 0;
return true;
}
if (value is null)
return false;
this[(slot, id.Value)] = 1ul << (2 * index + (value.Value ? 0 : 1));
return true;
}
public new void Clear()
{
base.Clear();
_allIds.SetAll(false);
}
public bool IsEmpty
=> !_allIds.HasAnySet() && Count is 0;
private static readonly int AllIndex = ShapeAttributeManager.ModelSlotSize * GenderRaceValues.Count;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool ToIndex(HumanSlot slot, GenderRace genderRace, out int index)
{
if (!GenderRaceIndices.TryGetValue(genderRace, out index))
return false;
index = ToIndex(slot, index);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool? Convert(bool trueValue, bool falseValue)
=> trueValue ? true : falseValue ? false : null;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool? Convert(ulong mask, int idx)
{
mask >>= idx;
return (mask & 3) switch
{
1 => true,
2 => false,
_ => null,
};
}
}

View file

@ -0,0 +1,106 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection)
{
public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
=> EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is true;
public bool ShouldBeDisabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
=> DisabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is false;
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> State(ShapeConnectorCondition connector)
=> connector switch
{
ShapeConnectorCondition.None => _shpData,
ShapeConnectorCondition.Wrists => _wristConnectors,
ShapeConnectorCondition.Waist => _waistConnectors,
ShapeConnectorCondition.Ankles => _ankleConnectors,
_ => [],
};
public int EnabledCount { get; private set; }
public int DisabledCount { get; private set; }
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _shpData = [];
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _wristConnectors = [];
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _waistConnectors = [];
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _ankleConnectors = [];
public void Reset()
{
Clear();
_shpData.Clear();
_wristConnectors.Clear();
_waistConnectors.Clear();
_ankleConnectors.Clear();
EnabledCount = 0;
DisabledCount = 0;
}
protected override void Dispose(bool _)
=> Reset();
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
{
switch (identifier.ConnectorCondition)
{
case ShapeConnectorCondition.None: Func(_shpData); break;
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
}
return;
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
{
if (!dict.TryGetValue(identifier.Shape, out var value))
{
value = [];
dict.Add(identifier.Shape, value);
}
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _))
{
if (entry.Value)
++EnabledCount;
else
++DisabledCount;
}
}
}
protected override void RevertModInternal(ShpIdentifier identifier)
{
switch (identifier.ConnectorCondition)
{
case ShapeConnectorCondition.None: Func(_shpData); break;
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
}
return;
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
{
if (!dict.TryGetValue(identifier.Shape, out var value))
return;
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which))
{
if (which)
--EnabledCount;
else
--DisabledCount;
if (value.IsEmpty)
dict.Remove(identifier.Shape);
}
}
}
}

View file

@ -0,0 +1,82 @@
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Interop;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Collections;
public sealed class CollectionAutoSelector : IService, IDisposable
{
private readonly Configuration _config;
private readonly ActiveCollections _collections;
private readonly IClientState _clientState;
private readonly CollectionResolver _resolver;
private readonly ObjectManager _objects;
public CollectionAutoSelector(Configuration config, ActiveCollections collections, IClientState clientState, CollectionResolver resolver,
ObjectManager objects)
{
_config = config;
_collections = collections;
_clientState = clientState;
_resolver = resolver;
_objects = objects;
if (_config.AutoSelectCollection)
Attach();
}
public bool Disposed { get; private set; }
public void SetAutomaticSelection(bool value)
{
_config.AutoSelectCollection = value;
if (value)
Attach();
else
Detach();
}
private void Attach()
{
if (Disposed)
return;
_clientState.Login += OnLogin;
Select();
}
private void OnLogin()
=> Select();
private void Detach()
=> _clientState.Login -= OnLogin;
private void Select()
{
if (!_objects[0].IsCharacter)
return;
var collection = _resolver.PlayerCollection();
if (collection.Identity.Id == Guid.Empty)
{
Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
}
else
{
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
_collections.SetCollection(collection, CollectionType.Current);
}
}
public void Dispose()
{
if (Disposed)
return;
Disposed = true;
Detach();
}
}

View file

@ -0,0 +1,28 @@
namespace Penumbra.Collections;
public struct CollectionCounters(int changeCounter)
{
/// <summary> Count the number of changes of the effective file list. </summary>
public int Change { get; private set; } = changeCounter;
/// <summary> Count the number of IMC-relevant changes of the effective file list. </summary>
public int Imc { get; private set; }
/// <summary> Count the number of ATCH-relevant changes of the effective file list. </summary>
public int Atch { get; private set; }
/// <summary> Increment the number of changes in the effective file list. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IncrementChange()
=> ++Change;
/// <summary> Increment the number of IMC-relevant changes in the effective file list. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IncrementImc()
=> ++Imc;
/// <summary> Increment the number of ATCH-relevant changes in the effective file list. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IncrementAtch()
=> ++Atch;
}

View file

@ -48,7 +48,7 @@ public static class ActiveCollectionMigration
if (!storage.ByName(collectionName, out var collection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning);
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning);
dict.Add(player, ModCollection.Empty);
}
else

View file

@ -1,8 +1,8 @@
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
@ -219,7 +219,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
_ => null,
};
if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count)
if (oldCollection == null || collection == oldCollection || collection.Identity.Index >= _storage.Count)
return;
switch (collectionType)
@ -262,13 +262,13 @@ public class ActiveCollections : ISavable, IDisposable, IService
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Id },
{ nameof(Interface), Interface.Id },
{ nameof(Current), Current.Id },
{ nameof(Default), Default.Identity.Id },
{ nameof(Interface), Interface.Identity.Id },
{ nameof(Current), Current.Identity.Id },
};
foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Id);
jObj.Add(type.ToString(), collection.Identity.Id);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer);
@ -282,7 +282,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
.Prepend(Interface)
.Prepend(Default)
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
.SelectMany(c => c.Inheritance.FlatHierarchy).Contains(Current);
/// <summary> Save if any of the active collections is changed and set new collections to Current. </summary>
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3)
@ -300,7 +300,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (oldCollection == Interface)
SetCollection(ModCollection.Empty, CollectionType.Interface);
if (oldCollection == Current)
SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
SetCollection(Default.Identity.Index > ModCollection.Empty.Identity.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
for (var i = 0; i < SpecialCollections.Length; ++i)
{
@ -325,11 +325,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
{
var configChanged = false;
// Load the default collection. If the name does not exist take the empty collection.
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? ModCollection.Empty.Name;
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? ModCollection.Empty.Identity.Name;
if (!_storage.ByName(defaultName, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.",
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@ -340,11 +340,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Identity.Name;
if (!_storage.ByName(interfaceName, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@ -355,11 +355,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the current collection.
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? Default.Name;
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? Default.Identity.Name;
if (!_storage.ByName(currentName, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.",
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
@ -404,7 +404,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(defaultId, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.",
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@ -415,11 +415,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
var interfaceId = jObject[nameof(Interface)]?.ToObject<Guid>() ?? Default.Id;
var interfaceId = jObject[nameof(Interface)]?.ToObject<Guid>() ?? Default.Identity.Id;
if (!_storage.ById(interfaceId, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.",
$"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@ -430,11 +430,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the current collection.
var currentId = jObject[nameof(Current)]?.ToObject<Guid>() ?? _storage.DefaultNamed.Id;
var currentId = jObject[nameof(Current)]?.ToObject<Guid>() ?? _storage.DefaultNamed.Identity.Id;
if (!_storage.ById(currentId, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.",
$"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
@ -587,7 +587,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
{
var global = ByType(CollectionType.Individual, _actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
return global?.Index == checkAssignment.Index
return (global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
: string.Empty;
}
@ -596,12 +596,12 @@ public class ActiveCollections : ISavable, IDisposable, IService
{
var global = ByType(CollectionType.Individual,
_actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
if (global?.Index == checkAssignment.Index)
if ((global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index)
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
}
var unowned = ByType(CollectionType.Individual, _actors.CreateNpc(id.Kind, id.DataId));
return unowned?.Index == checkAssignment.Index
return (unowned != null ? unowned.Identity.Index : null) == checkAssignment.Identity.Index
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty;
}
@ -617,7 +617,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (maleNpc == null)
{
maleNpc = Default;
if (maleNpc.Index != checkAssignment.Index)
if (maleNpc.Identity.Index != checkAssignment.Identity.Index)
return string.Empty;
collection1 = CollectionType.Default;
@ -626,7 +626,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (femaleNpc == null)
{
femaleNpc = Default;
if (femaleNpc.Index != checkAssignment.Index)
if (femaleNpc.Identity.Index != checkAssignment.Identity.Index)
return string.Empty;
collection2 = CollectionType.Default;
@ -646,7 +646,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (assignment == null)
continue;
if (assignment.Index == checkAssignment.Index)
if (assignment.Identity.Index == checkAssignment.Identity.Index)
return
$"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
}

View file

@ -1,4 +1,4 @@
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Mods;
@ -26,12 +26,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
/// </summary>
public bool SetModState(ModCollection collection, Mod mod, bool newValue)
{
var oldValue = collection.Settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false;
var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Enabled ?? false;
if (newValue == oldValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
((List<ModSettings?>)collection.Settings)[mod.Index]!.Enabled = newValue;
collection.GetOwnSettings(mod.Index)!.Enabled = newValue;
InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True,
0);
return true;
@ -55,13 +55,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
var changes = false;
foreach (var mod in mods)
{
var oldValue = collection.Settings[mod.Index]?.Enabled;
var oldValue = collection.GetOwnSettings(mod.Index)?.Enabled;
if (newValue == oldValue)
continue;
FixInheritance(collection, mod, false);
((List<ModSettings?>)collection.Settings)[mod.Index]!.Enabled = newValue;
changes = true;
collection.GetOwnSettings(mod.Index)!.Enabled = newValue;
changes = true;
}
if (!changes)
@ -76,35 +76,64 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
/// </summary>
public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue)
{
var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default;
var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Priority ?? ModPriority.Default;
if (newValue == oldValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
((List<ModSettings?>)collection.Settings)[mod.Index]!.Priority = newValue;
collection.GetOwnSettings(mod.Index)!.Priority = newValue;
InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0);
return true;
}
/// <summary>
/// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary.
/// /// If the mod is currently inherited, stop the inheritance.
/// If the mod is currently inherited, stop the inheritance.
/// </summary>
public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue)
{
var settings = collection.Settings[mod.Index] != null
? collection.Settings[mod.Index]!.Settings
: collection[mod.Index].Settings?.Settings;
var settings = collection.GetInheritedSettings(mod.Index).Settings?.Settings;
var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings;
if (oldValue == newValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
((List<ModSettings?>)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue);
collection.GetOwnSettings(mod.Index)!.SetValue(mod, groupIdx, newValue);
InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx);
return true;
}
public bool SetTemporarySettings(ModCollection collection, Mod mod, TemporaryModSettings? settings, int key = 0)
{
key = settings?.Lock ?? key;
if (!CanSetTemporarySettings(collection, mod, key))
return false;
collection.Settings.SetTemporary(mod.Index, settings);
InvokeChange(collection, ModSettingChange.TemporarySetting, mod, Setting.Indefinite, 0);
return true;
}
public int ClearTemporarySettings(ModCollection collection, int key = 0)
{
var numRemoved = 0;
for (var i = 0; i < collection.Settings.Count; ++i)
{
if (collection.GetTempSettings(i) is { } tempSettings
&& tempSettings.Lock == key
&& SetTemporarySettings(collection, modStorage[i], null, key))
++numRemoved;
}
return numRemoved;
}
public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key)
{
var old = collection.GetTempSettings(mod.Index);
return old is not { Lock: > 0 } || old.Lock == key;
}
/// <summary> Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. </summary>
public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName)
{
@ -115,10 +144,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
// If it does not exist, check unused settings.
// If it does not exist and has no unused settings, also use null.
ModSettings.SavedSettings? savedSettings = sourceMod != null
? collection.Settings[sourceMod.Index] != null
? new ModSettings.SavedSettings(collection.Settings[sourceMod.Index]!, sourceMod)
? collection.GetOwnSettings(sourceMod.Index) is { } ownSettings
? new ModSettings.SavedSettings(ownSettings, sourceMod)
: null
: collection.UnusedSettings.TryGetValue(sourceName, out var s)
: collection.Settings.Unused.TryGetValue(sourceName, out var s)
? s
: null;
@ -148,10 +177,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
// or remove any unused settings for the target if they are inheriting.
if (savedSettings != null)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings)[targetName] = savedSettings.Value;
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused)[targetName] = savedSettings.Value;
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
else if (((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(targetName))
else if (((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(targetName))
{
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
@ -166,12 +195,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
/// </summary>
private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit)
{
var settings = collection.Settings[mod.Index];
var settings = collection.GetOwnSettings(mod.Index);
if (inherit == (settings == null))
return false;
((List<ModSettings?>)collection.Settings)[mod.Index] =
inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod);
var settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod);
collection.Settings.Set(mod.Index, settings1);
return true;
}
@ -179,16 +208,18 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
if (type is not ModSettingChange.TemporarySetting)
saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false);
RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx);
if (type is not ModSettingChange.TemporarySetting)
RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx);
}
/// <summary> Trigger changes in all inherited collections. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
foreach (var directInheritor in directParent.DirectParentOf)
foreach (var directInheritor in directParent.Inheritance.DirectlyInheritedBy)
{
switch (type)
{
@ -197,7 +228,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true);
break;
default:
if (directInheritor.Settings[mod!.Index] == null)
if (directInheritor.GetOwnSettings(mod!.Index) == null)
communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true);
break;
}

View file

@ -1,6 +1,6 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;
@ -41,8 +41,8 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> inheritances)
{
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, CurrentCollectionId, version, Count, allSettings,
inheritances);
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
@ -57,7 +57,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
}
public void Delete(ModCollection collection)
=> _collectionsByLocal.Remove(collection.LocalId);
=> _collectionsByLocal.Remove(collection.Identity.LocalId);
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections =
@ -92,7 +92,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
{
if (name.Length != 0)
return _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
collection = ModCollection.Empty;
return true;
@ -102,7 +102,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
{
if (id != Guid.Empty)
return _collections.FindFirst(c => c.Id == id, out collection);
return _collections.FindFirst(c => c.Identity.Id == id, out collection);
collection = ModCollection.Empty;
return true;
@ -158,7 +158,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
var newCollection = Create(name, _collections.Count, duplicate);
_collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false);
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
return true;
}
@ -168,13 +168,13 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
/// </summary>
public bool RemoveCollection(ModCollection collection)
{
if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count)
if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
{
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
return false;
}
if (collection.Index == DefaultNamed.Index)
if (collection.Identity.Index == DefaultNamed.Identity.Index)
{
Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false);
return false;
@ -182,30 +182,34 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
Delete(collection);
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
_collections.RemoveAt(collection.Index);
_collections.RemoveAt(collection.Identity.Index);
// Update indices.
for (var i = collection.Index; i < Count; ++i)
_collections[i].Index = i;
_collectionsByLocal.Remove(collection.LocalId);
for (var i = collection.Identity.Index; i < Count; ++i)
_collections[i].Identity.Index = i;
_collectionsByLocal.Remove(collection.Identity.LocalId);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true;
}
/// <summary> Remove all settings for not currently-installed mods from the given collection. </summary>
public void CleanUnavailableSettings(ModCollection collection)
public int CleanUnavailableSettings(ModCollection collection)
{
var any = collection.UnusedSettings.Count > 0;
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Clear();
if (any)
var count = collection.Settings.Unused.Count;
if (count > 0)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear();
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
return count;
}
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
public void CleanUnavailableSetting(ModCollection collection, string? setting)
{
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(setting))
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
@ -246,13 +250,13 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.",
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
NotificationType.Warning);
}
catch (Exception ex)
{
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}",
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
NotificationType.Warning);
}
}
@ -273,7 +277,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.",
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
NotificationType.Error);
}
@ -291,14 +295,14 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
/// </summary>
private ModCollection SetDefaultNamedCollection()
{
if (ByName(ModCollection.DefaultCollectionName, out var collection))
if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
return collection;
if (AddCollection(ModCollection.DefaultCollectionName, null))
if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
return _collections[^1];
Penumbra.Messager.NotificationMessage(
$"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.",
$"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.",
NotificationType.Error);
return Count > 1 ? _collections[1] : _collections[0];
}
@ -307,7 +311,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
private void OnModDiscoveryStarted()
{
foreach (var collection in this)
collection.PrepareModDiscovery(_modStorage);
collection.Settings.PrepareModDiscovery(_modStorage);
}
/// <summary> Restore all settings in all collections to mods. </summary>
@ -315,7 +319,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
{
// Re-apply all mod settings.
foreach (var collection in this)
collection.ApplyModSettings(_saveService, _modStorage);
collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
}
/// <summary> Add or remove a mod from all collections, or re-save all collections where the mod has settings. </summary>
@ -326,21 +330,22 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
{
case ModPathChangeType.Added:
foreach (var collection in this)
collection.AddMod(mod);
collection.Settings.AddMod(mod);
break;
case ModPathChangeType.Deleted:
foreach (var collection in this)
collection.RemoveMod(mod);
collection.Settings.RemoveMod(mod);
break;
case ModPathChangeType.Moved:
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
foreach (var collection in this.Where(collection => collection.GetOwnSettings(mod.Index) != null))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
break;
case ModPathChangeType.Reloaded:
foreach (var collection in this)
{
if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false)
if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(mod.Index, null);
}
break;
@ -357,8 +362,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
foreach (var collection in this)
{
if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
if (collection.GetOwnSettings(mod.Index)?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(mod.Index, null);
}
}
@ -370,9 +376,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
foreach (var collection in this)
{
var (settings, _) = collection[mod.Index];
var (settings, _) = collection.GetActualSettings(mod.Index);
if (settings is { Enabled: true })
collection.IncrementCounter();
collection.Counters.IncrementChange();
}
}
}

View file

@ -18,7 +18,7 @@ public partial class IndividualCollections
foreach (var (name, identifiers, collection) in Assignments)
{
var tmp = identifiers[0].ToJson();
tmp.Add("Collection", collection.Id);
tmp.Add("Collection", collection.Identity.Id);
tmp.Add("Display", name);
ret.Add(tmp);
}
@ -182,7 +182,7 @@ public partial class IndividualCollections
Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}].");
else
Penumbra.Messager.NotificationMessage(
$"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.",
$"Could not migrate {name} ({collection.Identity.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.",
NotificationType.Error);
}
// If it is not a valid NPC name, check if it can be a player name.
@ -192,16 +192,16 @@ public partial class IndividualCollections
var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}."));
// Try to migrate the player name without logging full names.
if (Add($"{name} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", [identifier], collection))
Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier.");
Penumbra.Log.Information($"Migrated {shortName} ({collection.Identity.AnonymizedName}) to Player Identifier.");
else
Penumbra.Messager.NotificationMessage(
$"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.",
$"Could not migrate {shortName} ({collection.Identity.AnonymizedName}), please look through your individual collections.",
NotificationType.Error);
}
else
{
Penumbra.Messager.NotificationMessage(
$"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.",
$"Could not migrate {name} ({collection.Identity.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.",
NotificationType.Error);
}
}

View file

@ -1,7 +1,6 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
@ -63,10 +62,10 @@ public class InheritanceManager : IDisposable, IService
if (ReferenceEquals(potentialParent, potentialInheritor))
return ValidInheritance.Self;
if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent))
if (potentialInheritor.Inheritance.DirectlyInheritsFrom.Contains(potentialParent))
return ValidInheritance.Contained;
if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor)))
if (potentialParent.Inheritance.FlatHierarchy.Any(c => ReferenceEquals(c, potentialInheritor)))
return ValidInheritance.Circle;
return ValidInheritance.Valid;
@ -83,25 +82,23 @@ public class InheritanceManager : IDisposable, IService
/// <summary> Remove an existing inheritance from a collection. </summary>
public void RemoveInheritance(ModCollection inheritor, int idx)
{
var parent = inheritor.DirectlyInheritsFrom[idx];
((List<ModCollection>)inheritor.DirectlyInheritsFrom).RemoveAt(idx);
((List<ModCollection>)parent.DirectParentOf).Remove(inheritor);
var parent = inheritor.Inheritance.RemoveInheritanceAt(inheritor, idx);
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
RecurseInheritanceChanges(inheritor);
Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances.");
RecurseInheritanceChanges(inheritor, true);
Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.AnonymizedName} inheritances.");
}
/// <summary> Order in the inheritance list is relevant. </summary>
public void MoveInheritance(ModCollection inheritor, int from, int to)
{
if (!((List<ModCollection>)inheritor.DirectlyInheritsFrom).Move(from, to))
if (!inheritor.Inheritance.MoveInheritance(inheritor, from, to))
return;
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
RecurseInheritanceChanges(inheritor);
Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}.");
RecurseInheritanceChanges(inheritor, true);
Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}.");
}
/// <inheritdoc cref="AddInheritance(ModCollection, ModCollection)"/>
@ -110,16 +107,16 @@ public class InheritanceManager : IDisposable, IService
if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid)
return false;
((List<ModCollection>)inheritor.DirectlyInheritsFrom).Add(parent);
((List<ModCollection>)parent.DirectParentOf).Add(inheritor);
inheritor.Inheritance.AddInheritance(inheritor, parent);
if (invokeEvent)
{
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
RecurseInheritanceChanges(inheritor);
}
Penumbra.Log.Debug($"Added {parent.AnonymizedName} to {inheritor.AnonymizedName} inheritances.");
RecurseInheritanceChanges(inheritor, invokeEvent);
Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances.");
return true;
}
@ -131,11 +128,11 @@ public class InheritanceManager : IDisposable, IService
{
foreach (var collection in _storage)
{
if (collection.InheritanceByName == null)
if (collection.Inheritance.ConsumeNames() is not { } byName)
continue;
var changes = false;
foreach (var subCollectionName in collection.InheritanceByName)
foreach (var subCollectionName in byName)
{
if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection))
{
@ -143,29 +140,30 @@ public class InheritanceManager : IDisposable, IService
continue;
changes = true;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.",
Penumbra.Messager.NotificationMessage(
$"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.",
NotificationType.Warning);
}
else if (_storage.ByName(subCollectionName, out subCollection))
{
changes = true;
Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID.");
Penumbra.Log.Information($"Migrating inheritance for {collection.Identity.AnonymizedName} from name to GUID.");
if (AddInheritance(collection, subCollection, false))
continue;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.",
Penumbra.Messager.NotificationMessage(
$"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.",
NotificationType.Warning);
}
else
{
Penumbra.Messager.NotificationMessage(
$"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.",
$"Inherited collection {subCollectionName} for {collection.Identity.AnonymizedName} does not exist, it was removed.",
NotificationType.Warning);
changes = true;
}
}
collection.InheritanceByName = null;
if (changes)
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection));
}
@ -178,20 +176,22 @@ public class InheritanceManager : IDisposable, IService
foreach (var c in _storage)
{
var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old);
var inheritedIdx = c.Inheritance.DirectlyInheritsFrom.IndexOf(old);
if (inheritedIdx >= 0)
RemoveInheritance(c, inheritedIdx);
((List<ModCollection>)c.DirectParentOf).Remove(old);
c.Inheritance.RemoveChild(old);
}
}
private void RecurseInheritanceChanges(ModCollection newInheritor)
private void RecurseInheritanceChanges(ModCollection newInheritor, bool invokeEvent)
{
foreach (var inheritor in newInheritor.DirectParentOf)
foreach (var inheritor in newInheritor.Inheritance.DirectlyInheritedBy)
{
_communicator.CollectionInheritanceChanged.Invoke(inheritor, true);
RecurseInheritanceChanges(inheritor);
ModCollectionInheritance.UpdateFlattenedInheritance(inheritor);
RecurseInheritanceChanges(inheritor, invokeEvent);
if (invokeEvent)
_communicator.CollectionInheritanceChanged.Invoke(inheritor, true);
}
}
}

View file

@ -26,12 +26,12 @@ internal static class ModCollectionMigration
// Remove all completely defaulted settings from active and inactive mods.
for (var i = 0; i < collection.Settings.Count; ++i)
{
if (SettingIsDefaultV0(collection.Settings[i]))
((List<ModSettings?>)collection.Settings)[i] = null;
if (SettingIsDefaultV0(collection.GetOwnSettings(i)))
collection.Settings.SetAll(i, FullModSettings.Empty);
}
foreach (var (key, _) in collection.UnusedSettings.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList())
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(key);
foreach (var (key, _) in collection.Settings.Unused.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList())
collection.Settings.RemoveUnused(key);
return true;
}

View file

@ -1,4 +1,4 @@
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Communication;
@ -44,7 +44,7 @@ public class TempCollectionManager : IDisposable, IService
=> _customCollections.Values;
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection);
=> _customCollections.Values.FindFirst(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase), out collection);
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(id, out collection);
@ -54,12 +54,12 @@ public class TempCollectionManager : IDisposable, IService
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++);
Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}.");
if (_customCollections.TryAdd(collection.Id, collection))
Penumbra.Log.Debug($"Creating temporary collection {collection.Identity.Name} with {collection.Identity.Id}.");
if (_customCollections.TryAdd(collection.Identity.Id, collection))
{
// Temporary collection created.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty);
return collection.Id;
return collection.Identity.Id;
}
return Guid.Empty;
@ -74,8 +74,8 @@ public class TempCollectionManager : IDisposable, IService
}
_storage.Delete(collection);
Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}.");
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
Penumbra.Log.Debug($"Deleted temporary collection {collection.Identity.Id}.");
GlobalChangeCounter += Math.Max(collection.Counters.Change + 1 - GlobalChangeCounter, 0);
for (var i = 0; i < Collections.Count; ++i)
{
if (Collections[i].Collection != collection)
@ -83,7 +83,7 @@ public class TempCollectionManager : IDisposable, IService
// Temporary collection assignment removed.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}.");
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Identity.Id} from {Collections[i].DisplayName}.");
Collections.Delete(i--);
}
@ -96,7 +96,7 @@ public class TempCollectionManager : IDisposable, IService
return false;
// Temporary collection assignment added.
Penumbra.Log.Verbose($"Assigned temporary collection {collection.AnonymizedName} to {Collections.Last().DisplayName}.");
Penumbra.Log.Verbose($"Assigned temporary collection {collection.Identity.AnonymizedName} to {Collections.Last().DisplayName}.");
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName);
return true;
}
@ -127,6 +127,6 @@ public class TempCollectionManager : IDisposable, IService
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Identity.Id);
}
}

View file

@ -46,8 +46,8 @@ public partial class ModCollection
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();

View file

@ -1,4 +1,3 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Settings;
@ -13,111 +12,83 @@ namespace Penumbra.Collections;
/// - Index is the collections index in the ModCollection.Manager
/// - Settings has the same size as ModManager.Mods.
/// - any change in settings or inheritance of the collection causes a Save.
/// - the name can not contain invalid path characters and has to be unique when lower-cased.
/// </summary>
public partial class ModCollection
{
public const int CurrentVersion = 2;
public const string DefaultCollectionName = "Default";
public const string EmptyCollectionName = "None";
public const int CurrentVersion = 2;
/// <summary>
/// Create the always available Empty Collection that will always sit at index 0,
/// can not be deleted and does never create a cache.
/// </summary>
public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, LocalCollectionId.Zero, 0, 0, CurrentVersion, [], [], []);
public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, new ModSettingProvider(),
new ModCollectionInheritance());
/// <summary> The name of a collection. </summary>
public string Name { get; set; }
public Guid Id { get; }
public LocalCollectionId LocalId { get; }
public string Identifier
=> Id.ToString();
public string ShortIdentifier
=> Identifier[..8];
public ModCollectionIdentity Identity;
public override string ToString()
=> Name.Length > 0 ? Name : ShortIdentifier;
=> Identity.ToString();
/// <summary> Get the first two letters of a collection name and its Index (or None if it is the empty collection). </summary>
public string AnonymizedName
=> this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier;
public readonly ModSettingProvider Settings;
public ModCollectionInheritance Inheritance;
public CollectionCounters Counters;
/// <summary> The index of the collection is set and kept up-to-date by the CollectionManager. </summary>
public int Index { get; internal set; }
/// <summary>
/// Count the number of changes of the effective file list.
/// This is used for material and imc changes.
/// </summary>
public int ChangeCounter { get; private set; }
public uint ImcChangeCounter { get; set; }
public uint AtchChangeCounter { get; set; }
/// <summary> Increment the number of changes in the effective file list. </summary>
public int IncrementCounter()
=> ++ChangeCounter;
/// <summary>
/// If a ModSetting is null, it can be inherited from other collections.
/// If no collection provides a setting for the mod, it is just disabled.
/// </summary>
public readonly IReadOnlyList<ModSettings?> Settings;
/// <summary> Settings for deleted mods will be kept via the mods identifier (directory name). </summary>
public readonly IReadOnlyDictionary<string, ModSettings.SavedSettings> UnusedSettings;
/// <summary> Inheritances stored before they can be applied. </summary>
public IReadOnlyList<string>? InheritanceByName;
/// <summary> Contains all direct parent collections this collection inherits settings from. </summary>
public readonly IReadOnlyList<ModCollection> DirectlyInheritsFrom;
/// <summary> Contains all direct child collections that inherit from this collection. </summary>
public readonly IReadOnlyList<ModCollection> DirectParentOf = new List<ModCollection>();
/// <summary> All inherited collections in application order without filtering for duplicates. </summary>
public static IEnumerable<ModCollection> InheritedCollections(ModCollection collection)
=> collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection);
/// <summary>
/// Iterate over all collections inherited from in depth-first order.
/// Skip already visited collections to avoid circular dependencies.
/// </summary>
public IEnumerable<ModCollection> GetFlattenedInheritance()
=> InheritedCollections(this).Distinct();
/// <summary>
/// Obtain the actual settings for a given mod via index.
/// Also returns the collection the settings are taken from.
/// If no collection provides settings for this mod, this collection is returned together with null.
/// </summary>
public (ModSettings? Settings, ModCollection Collection) this[Index idx]
public ModSettings? GetOwnSettings(Index idx)
{
get
if (Identity.Index <= 0)
return ModSettings.Empty;
return Settings.Settings[idx].Settings;
}
public TemporaryModSettings? GetTempSettings(Index idx)
{
if (Identity.Index <= 0)
return null;
return Settings.Settings[idx].TempSettings;
}
public (ModSettings? Settings, ModCollection Collection) GetInheritedSettings(Index idx)
{
if (Identity.Index <= 0)
return (ModSettings.Empty, this);
foreach (var collection in Inheritance.FlatHierarchy)
{
if (Index <= 0)
return (ModSettings.Empty, this);
foreach (var collection in GetFlattenedInheritance())
{
var settings = collection.Settings[idx];
if (settings != null)
return (settings, collection);
}
return (null, this);
var settings = collection.Settings.Settings[idx].Settings;
if (settings != null)
return (settings, collection);
}
return (null, this);
}
public (ModSettings? Settings, ModCollection Collection) GetActualSettings(Index idx)
{
if (Identity.Index <= 0)
return (ModSettings.Empty, this);
// Check temp settings.
var ownTempSettings = Settings.Settings[idx].Resolve();
if (ownTempSettings != null)
return (ownTempSettings, this);
// Ignore temp settings for inherited collections.
foreach (var collection in Inheritance.FlatHierarchy.Skip(1))
{
var settings = collection.Settings.Settings[idx].Settings;
if (settings != null)
return (settings, collection);
}
return (null, this);
}
/// <summary> Evaluates all settings along the whole inheritance tree. </summary>
public IEnumerable<ModSettings?> ActualSettings
=> Enumerable.Range(0, Settings.Count).Select(i => this[i].Settings);
=> Enumerable.Range(0, Settings.Count).Select(i => GetActualSettings(i).Settings);
/// <summary>
/// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists.
@ -125,21 +96,16 @@ public partial class ModCollection
public ModCollection Duplicate(string name, LocalCollectionId localId, int index)
{
Debug.Assert(index > 0, "Collection duplicated with non-positive index.");
return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
[.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()));
return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, Settings.Clone(), Inheritance.Clone());
}
/// <summary> Constructor for reading from files. </summary>
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, LocalCollectionId localId, int version,
int index,
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, ModCollectionIdentity identity, int version,
Dictionary<string, ModSettings.SavedSettings> allSettings, IReadOnlyList<string> inheritances)
{
Debug.Assert(index > 0, "Collection read with non-positive index.");
var ret = new ModCollection(id, name, localId, index, 0, version, [], [], allSettings)
{
InheritanceByName = inheritances,
};
ret.ApplyModSettings(saver, mods);
Debug.Assert(identity.Index > 0, "Collection read with non-positive index.");
var ret = new ModCollection(identity, 0, version, new ModSettingProvider(allSettings), new ModCollectionInheritance(inheritances));
ret.Settings.ApplyModSettings(ret, saver, mods);
ModCollectionMigration.Migrate(saver, mods, version, ret);
return ret;
}
@ -148,7 +114,8 @@ public partial class ModCollection
public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter)
{
Debug.Assert(index < 0, "Temporary collection created with non-negative index.");
var ret = new ModCollection(Guid.NewGuid(), name, localId, index, changeCounter, CurrentVersion, [], [], []);
var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, new ModSettingProvider(),
new ModCollectionInheritance());
return ret;
}
@ -156,68 +123,18 @@ public partial class ModCollection
public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount)
{
Debug.Assert(index >= 0, "Empty collection created with negative index.");
return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion,
Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [],
[]);
return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, ModSettingProvider.Empty(modCount),
new ModCollectionInheritance());
}
/// <summary> Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. </summary>
internal bool AddMod(Mod mod)
private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, ModSettingProvider settings,
ModCollectionInheritance inheritance)
{
if (UnusedSettings.TryGetValue(mod.ModPath.Name, out var save))
{
var ret = save.ToSettings(mod, out var settings);
((List<ModSettings?>)Settings).Add(settings);
((Dictionary<string, ModSettings.SavedSettings>)UnusedSettings).Remove(mod.ModPath.Name);
return ret;
}
((List<ModSettings?>)Settings).Add(null);
return false;
}
/// <summary> Move settings from the current mod list to the unused mod settings. </summary>
internal void RemoveMod(Mod mod)
{
var settings = Settings[mod.Index];
if (settings != null)
((Dictionary<string, ModSettings.SavedSettings>)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod);
((List<ModSettings?>)Settings).RemoveAt(mod.Index);
}
/// <summary> Move all settings to unused settings for rediscovery. </summary>
internal void PrepareModDiscovery(ModStorage mods)
{
foreach (var (mod, setting) in mods.Zip(Settings).Where(s => s.Second != null))
((Dictionary<string, ModSettings.SavedSettings>)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod);
((List<ModSettings?>)Settings).Clear();
}
/// <summary>
/// Apply all mod settings from unused settings to the current set of mods.
/// Also fixes invalid settings.
/// </summary>
internal void ApplyModSettings(SaveService saver, ModStorage mods)
{
((List<ModSettings?>)Settings).Capacity = Math.Max(((List<ModSettings?>)Settings).Capacity, mods.Count);
if (mods.Aggregate(false, (current, mod) => current | AddMod(mod)))
saver.ImmediateSave(new ModCollectionSave(mods, this));
}
private ModCollection(Guid id, string name, LocalCollectionId localId, int index, int changeCounter, int version,
List<ModSettings?> appliedSettings, List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
{
Name = name;
Id = id;
LocalId = localId;
Index = index;
ChangeCounter = changeCounter;
Settings = appliedSettings;
UnusedSettings = settings;
DirectlyInheritsFrom = inheritsFrom;
foreach (var c in DirectlyInheritsFrom)
((List<ModCollection>)c.DirectParentOf).Add(this);
Identity = identity;
Counters = new CollectionCounters(changeCounter);
Settings = settings;
Inheritance = inheritance;
ModCollectionInheritance.UpdateChildren(this);
ModCollectionInheritance.UpdateFlattenedInheritance(this);
}
}

View file

@ -0,0 +1,43 @@
using OtterGui;
using OtterGui.Extensions;
using Penumbra.Collections.Manager;
namespace Penumbra.Collections;
public struct ModCollectionIdentity(Guid id, LocalCollectionId localId)
{
public const string DefaultCollectionName = "Default";
public const string EmptyCollectionName = "None";
public static readonly ModCollectionIdentity Empty = new(Guid.Empty, LocalCollectionId.Zero, EmptyCollectionName, 0);
public string Name { get; set; } = string.Empty;
public Guid Id { get; } = id;
public LocalCollectionId LocalId { get; } = localId;
/// <summary> The index of the collection is set and kept up-to-date by the CollectionManager. </summary>
public int Index { get; internal set; }
public string Identifier
=> Id.ToString();
public string ShortIdentifier
=> Id.ShortGuid();
/// <summary> Get the short identifier of a collection unless it is a well-known collection name. </summary>
public string AnonymizedName
=> Id == Guid.Empty ? EmptyCollectionName : Name == DefaultCollectionName ? Name : ShortIdentifier;
public override string ToString()
=> Name.Length > 0 ? Name : ShortIdentifier;
public ModCollectionIdentity(Guid id, LocalCollectionId localId, string name, int index)
: this(id, localId)
{
Name = name;
Index = index;
}
public static ModCollectionIdentity New(string name, LocalCollectionId id, int index)
=> new(Guid.NewGuid(), id, name, index);
}

View file

@ -0,0 +1,92 @@
using OtterGui.Filesystem;
namespace Penumbra.Collections;
public struct ModCollectionInheritance
{
public IReadOnlyList<string>? InheritanceByName { get; private set; }
private readonly List<ModCollection> _directlyInheritsFrom = [];
private readonly List<ModCollection> _directlyInheritedBy = [];
private readonly List<ModCollection> _flatHierarchy = [];
public ModCollectionInheritance()
{ }
private ModCollectionInheritance(List<ModCollection> inheritsFrom)
=> _directlyInheritsFrom = [.. inheritsFrom];
public ModCollectionInheritance(IReadOnlyList<string> byName)
=> InheritanceByName = byName;
public ModCollectionInheritance Clone()
=> new(_directlyInheritsFrom);
public IEnumerable<string> Identifiers
=> InheritanceByName ?? _directlyInheritsFrom.Select(c => c.Identity.Identifier);
public IReadOnlyList<string>? ConsumeNames()
{
var ret = InheritanceByName;
InheritanceByName = null;
return ret;
}
public static void UpdateChildren(ModCollection parent)
{
foreach (var inheritance in parent.Inheritance.DirectlyInheritsFrom)
inheritance.Inheritance._directlyInheritedBy.Add(parent);
}
public void AddInheritance(ModCollection inheritor, ModCollection newParent)
{
_directlyInheritsFrom.Add(newParent);
newParent.Inheritance._directlyInheritedBy.Add(inheritor);
UpdateFlattenedInheritance(inheritor);
}
public ModCollection RemoveInheritanceAt(ModCollection inheritor, int idx)
{
var parent = DirectlyInheritsFrom[idx];
_directlyInheritsFrom.RemoveAt(idx);
parent.Inheritance._directlyInheritedBy.Remove(parent);
UpdateFlattenedInheritance(inheritor);
return parent;
}
public bool MoveInheritance(ModCollection inheritor, int from, int to)
{
if (!_directlyInheritsFrom.Move(from, to))
return false;
UpdateFlattenedInheritance(inheritor);
return true;
}
public void RemoveChild(ModCollection child)
=> _directlyInheritedBy.Remove(child);
/// <summary> Contains all direct parent collections this collection inherits settings from. </summary>
public readonly IReadOnlyList<ModCollection> DirectlyInheritsFrom
=> _directlyInheritsFrom;
/// <summary> Contains all direct child collections that inherit from this collection. </summary>
public readonly IReadOnlyList<ModCollection> DirectlyInheritedBy
=> _directlyInheritedBy;
/// <summary>
/// Iterate over all collections inherited from in depth-first order.
/// Skip already visited collections to avoid circular dependencies.
/// </summary>
public readonly IReadOnlyList<ModCollection> FlatHierarchy
=> _flatHierarchy;
public static void UpdateFlattenedInheritance(ModCollection parent)
{
parent.Inheritance._flatHierarchy.Clear();
parent.Inheritance._flatHierarchy.AddRange(InheritedCollections(parent).Distinct());
}
/// <summary> All inherited collections in application order without filtering for duplicates. </summary>
private static IEnumerable<ModCollection> InheritedCollections(ModCollection parent)
=> parent.Inheritance.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(parent);
}

View file

@ -15,7 +15,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
=> fileNames.CollectionFile(modCollection);
public string LogName(string _)
=> modCollection.AnonymizedName;
=> modCollection.Identity.AnonymizedName;
public string TypeName
=> "Collection";
@ -28,23 +28,23 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
j.WriteStartObject();
j.WritePropertyName("Version");
j.WriteValue(ModCollection.CurrentVersion);
j.WritePropertyName(nameof(ModCollection.Id));
j.WriteValue(modCollection.Identifier);
j.WritePropertyName(nameof(ModCollection.Name));
j.WriteValue(modCollection.Name);
j.WritePropertyName(nameof(ModCollection.Settings));
j.WritePropertyName(nameof(ModCollectionIdentity.Id));
j.WriteValue(modCollection.Identity.Identifier);
j.WritePropertyName(nameof(ModCollectionIdentity.Name));
j.WriteValue(modCollection.Identity.Name);
j.WritePropertyName("Settings");
// Write all used and unused settings by mod directory name.
j.WriteStartObject();
var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.UnusedSettings.Count);
var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.Settings.Unused.Count);
for (var i = 0; i < modCollection.Settings.Count; ++i)
{
var settings = modCollection.Settings[i];
var settings = modCollection.GetOwnSettings(i);
if (settings != null)
list.Add((modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, modStorage[i])));
}
list.AddRange(modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value)));
list.AddRange(modCollection.Settings.Unused.Select(kvp => (kvp.Key, kvp.Value)));
list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase));
foreach (var (modDir, settings) in list)
@ -57,7 +57,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
// Inherit by collection name.
j.WritePropertyName("Inheritance");
x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier));
x.Serialize(j, modCollection.Inheritance.Identifiers);
j.WriteEndObject();
}
@ -79,10 +79,10 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
{
var obj = JObject.Parse(File.ReadAllText(file.FullName));
version = obj["Version"]?.ToObject<int>() ?? 0;
name = obj[nameof(ModCollection.Name)]?.ToObject<string>() ?? string.Empty;
id = obj[nameof(ModCollection.Id)]?.ToObject<Guid>() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty);
name = obj[nameof(ModCollectionIdentity.Name)]?.ToObject<string>() ?? string.Empty;
id = obj[nameof(ModCollectionIdentity.Id)]?.ToObject<Guid>() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty);
// Custom deserialization that is converted with the constructor.
settings = obj[nameof(ModCollection.Settings)]?.ToObject<Dictionary<string, ModSettings.SavedSettings>>() ?? settings;
settings = obj["Settings"]?.ToObject<Dictionary<string, ModSettings.SavedSettings>>() ?? settings;
inheritance = obj["Inheritance"]?.ToObject<List<string>>() ?? inheritance;
return true;
}

View file

@ -0,0 +1,98 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections;
public readonly struct ModSettingProvider
{
private ModSettingProvider(IEnumerable<FullModSettings> settings, Dictionary<string, ModSettings.SavedSettings> unusedSettings)
{
_settings = settings.Select(s => s.DeepCopy()).ToList();
_unused = unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy());
}
public ModSettingProvider()
{ }
public static ModSettingProvider Empty(int count)
=> new(Enumerable.Repeat(FullModSettings.Empty, count), []);
public ModSettingProvider(Dictionary<string, ModSettings.SavedSettings> allSettings)
=> _unused = allSettings;
private readonly List<FullModSettings> _settings = [];
/// <summary> Settings for deleted mods will be kept via the mods identifier (directory name). </summary>
private readonly Dictionary<string, ModSettings.SavedSettings> _unused = [];
public int Count
=> _settings.Count;
public bool RemoveUnused(string key)
=> _unused.Remove(key);
internal void Set(Index index, ModSettings? settings)
=> _settings[index] = _settings[index] with { Settings = settings };
internal void SetTemporary(Index index, TemporaryModSettings? settings)
=> _settings[index] = _settings[index] with { TempSettings = settings };
internal void SetAll(Index index, FullModSettings settings)
=> _settings[index] = settings;
public IReadOnlyList<FullModSettings> Settings
=> _settings;
public IReadOnlyDictionary<string, ModSettings.SavedSettings> Unused
=> _unused;
public ModSettingProvider Clone()
=> new(_settings, _unused);
/// <summary> Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. </summary>
internal bool AddMod(Mod mod)
{
if (_unused.Remove(mod.ModPath.Name, out var save))
{
var ret = save.ToSettings(mod, out var settings);
_settings.Add(new FullModSettings(settings));
return ret;
}
_settings.Add(FullModSettings.Empty);
return false;
}
/// <summary> Move settings from the current mod list to the unused mod settings. </summary>
internal void RemoveMod(Mod mod)
{
var settings = _settings[mod.Index];
if (settings.Settings != null)
_unused[mod.ModPath.Name] = new ModSettings.SavedSettings(settings.Settings, mod);
_settings.RemoveAt(mod.Index);
}
/// <summary> Move all settings to unused settings for rediscovery. </summary>
internal void PrepareModDiscovery(ModStorage mods)
{
foreach (var (mod, setting) in mods.Zip(_settings).Where(s => s.Second.Settings != null))
_unused[mod.ModPath.Name] = new ModSettings.SavedSettings(setting.Settings!, mod);
_settings.Clear();
}
/// <summary>
/// Apply all mod settings from unused settings to the current set of mods.
/// Also fixes invalid settings.
/// </summary>
internal void ApplyModSettings(ModCollection parent, SaveService saver, ModStorage mods)
{
_settings.Capacity = Math.Max(_settings.Capacity, mods.Count);
var settings = this;
if (mods.Aggregate(false, (current, mod) => current | settings.AddMod(mod)))
saver.ImmediateSave(new ModCollectionSave(mods, parent));
}
}

View file

@ -23,7 +23,7 @@ public readonly struct ResolveData(ModCollection collection, nint gameObject)
{ }
public override string ToString()
=> ModCollection.Name;
=> ModCollection.Identity.Name;
}
public static class ResolveDataExtensions

View file

@ -1,9 +1,10 @@
using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -74,20 +75,21 @@ public class CommandHandler : IDisposable, IApiService
_ = argumentList[0].ToLowerInvariant() switch
{
"window" => ToggleWindow(arguments),
"enable" => SetPenumbraState(arguments, true),
"disable" => SetPenumbraState(arguments, false),
"toggle" => SetPenumbraState(arguments, null),
"reload" => Reload(arguments),
"redraw" => Redraw(arguments),
"lockui" => SetUiLockState(arguments),
"size" => SetUiMinimumSize(arguments),
"debug" => SetDebug(arguments),
"collection" => SetCollection(arguments),
"mod" => SetMod(arguments),
"bulktag" => SetTag(arguments),
"knowledge" => HandleKnowledge(arguments),
_ => PrintHelp(argumentList[0]),
"window" => ToggleWindow(arguments),
"enable" => SetPenumbraState(arguments, true),
"disable" => SetPenumbraState(arguments, false),
"toggle" => SetPenumbraState(arguments, null),
"reload" => Reload(arguments),
"redraw" => Redraw(arguments),
"lockui" => SetUiLockState(arguments),
"size" => SetUiMinimumSize(arguments),
"debug" => SetDebug(arguments),
"collection" => SetCollection(arguments),
"mod" => SetMod(arguments),
"bulktag" => SetTag(arguments),
"clearsettings" => ClearSettings(arguments),
"knowledge" => HandleKnowledge(arguments),
_ => PrintHelp(argumentList[0]),
};
}
@ -125,6 +127,21 @@ public class CommandHandler : IDisposable, IApiService
_chat.Print(new SeStringBuilder()
.AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.")
.BuiltString);
_chat.Print(new SeStringBuilder()
.AddCommand("clearsettings",
"Clear all temporary settings applied manually through Penumbra in the current or all collections. Use with 'all' parameter for all.")
.BuiltString);
return true;
}
private bool ClearSettings(string arguments)
{
if (arguments.Trim().ToLowerInvariant() is "all")
foreach (var collection in _collectionManager.Storage)
_collectionEditor.ClearTemporarySettings(collection);
else
_collectionEditor.ClearTemporarySettings(_collectionManager.Active.Current);
return true;
}
@ -325,7 +342,7 @@ public class CommandHandler : IDisposable, IApiService
{
_chat.Print(collection == null
? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned"
: $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
: $"{collection.Identity.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
continue;
}
@ -362,13 +379,13 @@ public class CommandHandler : IDisposable, IApiService
}
Print(
$"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}");
$"Removed {oldCollection.Identity.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}");
anySuccess = true;
continue;
}
_collectionManager.Active.SetCollection(collection!, type, individualIndex);
Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
Print($"Assigned {collection!.Identity.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
}
return anySuccess;
@ -379,16 +396,18 @@ public class CommandHandler : IDisposable, IApiService
if (arguments.Length == 0)
{
var seString = new SeStringBuilder()
.AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle]").AddText(" ").AddYellow("[Collection Name]")
.AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|").AddGreen("setting").AddBlue("]").AddText(" ")
.AddYellow("[Collection Name]")
.AddText(" | ")
.AddPurple("[Mod Name or Mod Directory Name]");
.AddPurple("[Mod Name or Mod Directory Name]")
.AddGreen(" <| [Option Group Name] | [Option1;Option2;...]>");
_chat.Print(seString.BuiltString);
return true;
}
var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var nameSplit = split.Length != 2
? Array.Empty<string>()
? []
: split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (nameSplit.Length != 2)
{
@ -406,6 +425,24 @@ public class CommandHandler : IDisposable, IApiService
if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty)
return false;
var groupName = string.Empty;
var optionNames = Array.Empty<string>();
if (state is 4)
{
var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (split2.Length < 2)
{
_chat.Print(
"Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options.");
return false;
}
nameSplit[1] = split2[0];
groupName = split2[1];
if (split2.Length == 3)
optionNames = split2[2].Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}
if (!_modManager.TryGetMod(nameSplit[1], nameSplit[1], out var mod))
{
_chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" does not exist.")
@ -413,12 +450,35 @@ public class CommandHandler : IDisposable, IApiService
return false;
}
if (HandleModState(state, collection!, mod))
return true;
if (state < 4)
{
if (HandleModState(state, collection!, mod))
return true;
_chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true)
.AddText("already had the desired state in collection ")
.AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString);
return false;
}
switch (ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIndex, out var setting))
{
case PenumbraApiEc.OptionGroupMissing:
_chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" has no group ")
.AddGreen(groupName, true).AddText(".").BuiltString);
return false;
case PenumbraApiEc.OptionMissing:
_chat.Print(new SeStringBuilder().AddText("Not all set options in the mod ").AddRed(nameSplit[1], true)
.AddText(" could be found in group ").AddGreen(groupName, true).AddText(".").BuiltString);
return false;
case PenumbraApiEc.Success:
_collectionEditor.SetModSetting(collection!, mod, groupIndex, setting);
Print(() => new SeStringBuilder().AddText("Changed settings of group ").AddGreen(groupName, true).AddText(" in mod ")
.AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString);
return true;
}
_chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true)
.AddText("already had the desired state in collection ")
.AddYellow(collection!.Name, true).AddText(".").BuiltString);
return false;
}
@ -500,7 +560,7 @@ public class CommandHandler : IDisposable, IApiService
changes |= HandleModState(state, collection!, mod);
if (!changes)
Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Name, true)
Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Identity.Name, true)
.AddText(".").BuiltString);
return true;
@ -515,7 +575,7 @@ public class CommandHandler : IDisposable, IApiService
return true;
}
collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase)
collection = string.Equals(lowerName, ModCollection.Empty.Identity.Name, StringComparison.OrdinalIgnoreCase)
? ModCollection.Empty
: _collectionManager.Storage.ByIdentifier(lowerName, out var c)
? c
@ -556,12 +616,14 @@ public class CommandHandler : IDisposable, IApiService
"toggle" => 2,
"inherit" => 3,
"inherited" => 3,
"setting" => 4,
"settings" => 4,
_ => -1,
};
private bool HandleModState(int settingState, ModCollection collection, Mod mod)
{
var settings = collection.Settings[mod.Index];
var settings = collection.GetOwnSettings(mod.Index);
switch (settingState)
{
case 0:
@ -569,7 +631,7 @@ public class CommandHandler : IDisposable, IApiService
return false;
Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection.Name, true)
.AddYellow(collection.Identity.Name, true)
.AddText(".").BuiltString);
return true;
@ -578,7 +640,7 @@ public class CommandHandler : IDisposable, IApiService
return false;
Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection.Name, true)
.AddYellow(collection.Identity.Name, true)
.AddText(".").BuiltString);
return true;
@ -589,7 +651,7 @@ public class CommandHandler : IDisposable, IApiService
Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true)
.AddText(" in collection ")
.AddYellow(collection.Name, true)
.AddYellow(collection.Identity.Name, true)
.AddText(".").BuiltString);
return true;
@ -598,7 +660,7 @@ public class CommandHandler : IDisposable, IApiService
return false;
Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection.Name, true)
.AddYellow(collection.Identity.Name, true)
.AddText(" to inherit.").BuiltString);
return true;
}

View file

@ -12,7 +12,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the clicked object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData, ChangedItemClick.Priority>(nameof(ChangedItemClick))
{
public enum Priority
{

View file

@ -10,7 +10,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{
public enum Priority
{

View file

@ -0,0 +1,23 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Interop.Services;
namespace Penumbra.Communication;
/// <summary>
/// Triggered when the Character Utility becomes ready.
/// </summary>
public sealed class CharacterUtilityFinished() : EventWrapper<CharacterUtilityFinished.Priority>(nameof(CharacterUtilityFinished))
{
public enum Priority
{
/// <seealso cref="CharacterUtility"/>
OnFinishedLoading = int.MaxValue,
/// <seealso cref="IpcProviders.OnCharacterUtilityReady"/>
IpcProvider = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager"/>
CollectionCacheManager = 0,
}
}

View file

@ -3,6 +3,7 @@ using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Communication;
@ -20,11 +21,14 @@ public sealed class ModPathChanged()
{
public enum Priority
{
/// <seealso cref="PcpService.OnModPathChange"/>
PcpService = int.MinValue,
/// <seealso cref="ModsApi.OnModPathChange"/>
ApiMods = int.MinValue,
ApiMods = int.MinValue + 1,
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
ApiModSettings = int.MinValue,
ApiModSettings = int.MinValue + 1,
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
EphemeralConfig = -500,

View file

@ -0,0 +1,21 @@
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
namespace Penumbra.Communication;
/// <summary>
/// Triggered when the character.json file for a .pcp file is written.
/// <list type="number">
/// <item>Parameter is the JObject that gets written to file. </item>
/// <item>Parameter is the object index of the game object this is written for. </item>
/// <item>Parameter is the full path to the directory being set up for the PCP creation. </item>
/// </list>
/// </summary>
public sealed class PcpCreation() : EventWrapper<JObject, ushort, string, PcpCreation.Priority>(nameof(PcpCreation))
{
public enum Priority
{
/// <seealso cref="Api.Api.ModsApi"/>
ModsApi = int.MinValue,
}
}

View file

@ -0,0 +1,21 @@
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
namespace Penumbra.Communication;
/// <summary>
/// Triggered when the character.json file for a .pcp file is parsed and applied.
/// <list type="number">
/// <item>Parameter is parsed JObject that contains the data. </item>
/// <item>Parameter is the identifier of the created mod. </item>
/// <item>Parameter is the GUID of the created collection. </item>
/// </list>
/// </summary>
public sealed class PcpParsing() : EventWrapper<JObject, string, Guid, PcpParsing.Priority>(nameof(PcpParsing))
{
public enum Priority
{
/// <seealso cref="Api.Api.ModsApi"/>
ModsApi = int.MinValue,
}
}

View file

@ -1,6 +1,5 @@
using OtterGui.Classes;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
@ -33,5 +32,8 @@ public sealed class ResolvedFileChanged()
{
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChange"/>
DalamudSubstitutionProvider = 0,
/// <seealso cref="Interop.Services.SchedulerResourceManagementService.OnResolvedFileChange"/>
SchedulerResourceManagementService = 0,
}
}

View file

@ -1,8 +1,8 @@
using Dalamud.Configuration;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Filesystem;
using OtterGui.Services;
using OtterGui.Widgets;
@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
public record PcpSettings
{
public bool CreateCollection { get; set; } = true;
public bool AssignCollection { get; set; } = true;
public bool AllowIpc { get; set; } = true;
public bool DisableHandling { get; set; } = false;
public string FolderName { get; set; } = "PCP";
}
[Serializable]
public class Configuration : IPluginConfiguration, ISavable, IService
{
@ -39,15 +48,12 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool EnableMods
{
get => _enableMods;
set
{
_enableMods = value;
ModsEnabled?.Invoke(value);
}
set => SetField(ref _enableMods, value, ModsEnabled);
}
public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { get; set; } = string.Empty;
public string WatchDirectory { get; set; } = string.Empty;
public bool? UseCrashHandler { get; set; } = null;
public bool OpenWindowAtStart { get; set; } = false;
@ -56,20 +62,28 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool HideUiWhenUiHidden { get; set; } = false;
public bool UseDalamudUiTextureRedirection { get; set; } = true;
public bool ShowModsInLobby { get; set; } = true;
public bool UseCharacterCollectionInMainWindow { get; set; } = true;
public bool UseCharacterCollectionsInCards { get; set; } = true;
public bool UseCharacterCollectionInInspect { get; set; } = true;
public bool UseCharacterCollectionInTryOn { get; set; } = true;
public bool UseOwnerNameForCharacterCollection { get; set; } = true;
public bool UseNoModsInInspect { get; set; } = false;
public bool HideChangedItemFilters { get; set; } = false;
public bool ReplaceNonAsciiOnImport { get; set; } = false;
public bool HidePrioritiesInSelector { get; set; } = false;
public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
public int OptionGroupCollapsibleMin { get; set; } = 5;
public bool AutoSelectCollection { get; set; } = false;
public bool ShowModsInLobby { get; set; } = true;
public bool UseCharacterCollectionInMainWindow { get; set; } = true;
public bool UseCharacterCollectionsInCards { get; set; } = true;
public bool UseCharacterCollectionInInspect { get; set; } = true;
public bool UseCharacterCollectionInTryOn { get; set; } = true;
public bool UseOwnerNameForCharacterCollection { get; set; } = true;
public bool UseNoModsInInspect { get; set; } = false;
public bool HideChangedItemFilters { get; set; } = false;
public bool ReplaceNonAsciiOnImport { get; set; } = false;
public bool HidePrioritiesInSelector { get; set; } = false;
public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public bool DefaultTemporaryMode { get; set; } = false;
public bool EnableDirectoryWatch { get; set; } = false;
public bool EnableAutomaticModImport { get; set; } = false;
public bool EnableCustomShapes { get; set; } = true;
public PcpSettings PcpSettings = new();
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
public int OptionGroupCollapsibleMin { get; set; } = 5;
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);
@ -84,9 +98,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
[JsonProperty(Order = int.MaxValue)]
public ISortMode<Mod> SortMode = ISortMode<Mod>.FoldersFirst;
public bool ScaleModSelector { get; set; } = false;
public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize;
public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize;
public bool OpenFoldersByDefault { get; set; } = false;
public int SingleGroupRadioMax { get; set; } = 2;
public string DefaultImportFolder { get; set; } = string.Empty;
@ -94,13 +105,14 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public string QuickMoveFolder2 { get; set; } = string.Empty;
public string QuickMoveFolder3 { get; set; } = string.Empty;
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control);
public bool PrintSuccessfulCommandsToChat { get; set; } = true;
public bool AutoDeduplicateOnImport { get; set; } = true;
public bool AutoReduplicateUiOnImport { get; set; } = true;
public bool UseFileSystemCompression { get; set; } = true;
public bool EnableHttpApi { get; set; } = true;
public bool MigrateImportedModelsToV6 { get; set; } = true;
public bool MigrateImportedModelsToV6 { get; set; } = true;
public bool MigrateImportedMaterialsToLegacy { get; set; } = true;
public string DefaultModImportPath { get; set; } = string.Empty;
@ -108,6 +120,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool KeepDefaultMetaChanges { get; set; } = false;
public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author;
public bool EditRawTileTransforms { get; set; } = false;
public bool HdrRenderTargets { get; set; } = true;
public Dictionary<ColorId, uint> Colors { get; set; }
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);
@ -213,4 +226,45 @@ public class Configuration : IPluginConfiguration, ISavable, IService
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool SetField<T>(ref T field, T value, Action<T, T>? @event, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(value))
return false;
var oldValue = field;
field = value;
try
{
@event?.Invoke(oldValue, field);
}
catch (Exception ex)
{
Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} from {oldValue} to {field}:\n{ex}");
throw;
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool SetField<T>(ref T field, T value, Action<T>? @event, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(value))
return false;
field = value;
try
{
@event?.Invoke(field);
}
catch (Exception ex)
{
Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} to {field}:\n{ex}");
throw;
}
return true;
}
}

View file

@ -0,0 +1,7 @@
namespace Penumbra;
public class DebugConfiguration
{
public static bool WriteImcBytesToLog = false;
public static bool UseSkinMaterialProcessing = true;
}

View file

@ -1,6 +1,7 @@
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using OtterGui.Classes;
using OtterGui.FileSystem.Selector;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
@ -23,6 +24,10 @@ public class EphemeralConfig : ISavable, IDisposable, IService
[JsonIgnore]
private readonly ModPathChanged _modPathChanged;
public float CurrentModSelectorWidth { get; set; } = 200f;
public float ModSelectorMinimumScale { get; set; } = 0.1f;
public float ModSelectorMaximumScale { get; set; } = 0.5f;
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false;
@ -41,6 +46,7 @@ public class EphemeralConfig : ISavable, IDisposable, IService
public string LastModPath { get; set; } = string.Empty;
public bool AdvancedEditingOpen { get; set; } = false;
public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false;
/// <summary>
/// Load the current configuration.

View file

@ -1,6 +1,7 @@
using Lumina.Data.Parsing;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.UI.AdvancedWindow.Materials;
using SharpGLTF.Materials;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Advanced;
@ -140,13 +141,13 @@ public class MaterialExporter
// Lerp between table row values to fetch final pixel values for each subtexture.
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend);
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
baseColorSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedDiffuse), 1));
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend);
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1));
specularSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedSpecularColor), 1));
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend);
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
emissiveSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedEmissive), 1));
}
}
}
@ -288,7 +289,7 @@ public class MaterialExporter
const uint valueFace = 0x6E5B8F10;
var isFace = material.Mtrl.ShaderPackage.ShaderKeys
.Any(key => key is { Category: categoryHairType, Value: valueFace });
.Any(key => key is { Key: categoryHairType, Value: valueFace });
var normal = material.Textures[TextureUsage.SamplerNormal];
var mask = material.Textures[TextureUsage.SamplerMask];
@ -363,7 +364,7 @@ public class MaterialExporter
// Face is the default for the skin shader, so a lack of skin type category is also correct.
var isFace = !material.Mtrl.ShaderPackage.ShaderKeys
.Any(key => key.Category == categorySkinType && key.Value != valueFace);
.Any(key => key.Key == categorySkinType && key.Value != valueFace);
// TODO: There's more nuance to skin than this, but this should be enough for a baseline reference.
// TODO: Specular?

View file

@ -2,12 +2,11 @@ using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using Lumina.Extensions;
using OtterGui;
using OtterGui.Extensions;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.IO;
using SharpGLTF.Materials;
using SharpGLTF.Scenes;
@ -84,9 +83,12 @@ public class MeshExporter
_boneIndexMap = BuildBoneIndexMap(skeleton.Value);
var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements
.GroupBy(ele => (MdlFile.VertexUsage)ele.Usage, ele => ele)
.ToImmutableDictionary(
element => (MdlFile.VertexUsage)element.Usage,
element => (MdlFile.VertexType)element.Type
g => g.Key,
g => g.OrderBy(ele => ele.UsageIndex) // OrderBy UsageIndex is probably unnecessary as they're probably already be in order
.Select(ele => (MdlFile.VertexType)ele.Type)
.ToList()
);
_geometryType = GetGeometryType(usages);
@ -112,6 +114,7 @@ public class MeshExporter
var indexMap = new Dictionary<ushort, int>();
// #TODO @ackwell maybe fix for V6 Models, I think this works fine.
foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex())
{
var boneName = _mdl.Bones[xivBoneIndex];
@ -278,18 +281,22 @@ public class MeshExporter
var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements
.OrderBy(element => element.Offset)
.Select(element => ((MdlFile.VertexUsage)element.Usage, element))
.ToList();
var vertices = new List<IVertexBuilder>();
var attributes = new Dictionary<MdlFile.VertexUsage, object>();
var attributes = new Dictionary<MdlFile.VertexUsage, List<object>>();
for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++)
{
attributes.Clear();
foreach (var (usage, element) in sortedElements)
attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]);
attributes = sortedElements
.GroupBy(element => element.Usage)
.ToDictionary(
x => (MdlFile.VertexUsage)x.Key,
x => x.OrderBy(ele => ele.UsageIndex) // Once again, OrderBy UsageIndex is probably unnecessary
.Select(ele => ReadVertexAttribute((MdlFile.VertexType)ele.Type, streams[ele.Stream]))
.ToList()
);
var vertexGeometry = BuildVertexGeometry(attributes);
var vertexMaterial = BuildVertexMaterial(attributes);
@ -320,7 +327,7 @@ public class MeshExporter
}
/// <summary> Get the vertex geometry type for this mesh's vertex usages. </summary>
private Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
private Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, List<MdlFile.VertexType>> usages)
{
if (!usages.ContainsKey(MdlFile.VertexUsage.Position))
throw _notifier.Exception("Mesh does not contain position vertex elements.");
@ -335,29 +342,29 @@ public class MeshExporter
}
/// <summary> Build a geometry vertex from a vertex's attributes. </summary>
private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary<MdlFile.VertexUsage, List<object>> attributes)
{
if (_geometryType == typeof(VertexPosition))
return new VertexPosition(
ToVector3(attributes[MdlFile.VertexUsage.Position])
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position))
);
if (_geometryType == typeof(VertexPositionNormal))
return new VertexPositionNormal(
ToVector3(attributes[MdlFile.VertexUsage.Position]),
ToVector3(attributes[MdlFile.VertexUsage.Normal])
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))
);
if (_geometryType == typeof(VertexPositionNormalTangent))
{
// (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range.
// TODO: While this assumption is safe, it would be sensible to actually check.
var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) * 2 - Vector4.One;
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
return new VertexPositionNormalTangent(
ToVector3(attributes[MdlFile.VertexUsage.Position]),
ToVector3(attributes[MdlFile.VertexUsage.Normal]),
bitangent
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)),
bitangent.SanitizeTangent()
);
}
@ -365,60 +372,90 @@ public class MeshExporter
}
/// <summary> Get the vertex material type for this mesh's vertex usages. </summary>
private Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
private Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, List<MdlFile.VertexType>> usages)
{
var uvCount = 0;
if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type))
uvCount = type switch
if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var list))
{
foreach (var type in list)
{
MdlFile.VertexType.Half2 => 1,
MdlFile.VertexType.Half4 => 2,
MdlFile.VertexType.Single2 => 1,
MdlFile.VertexType.Single4 => 2,
_ => throw _notifier.Exception($"Unexpected UV vertex type {type}."),
};
uvCount += type switch
{
MdlFile.VertexType.Half2 => 1,
MdlFile.VertexType.Half4 => 2,
MdlFile.VertexType.Single2 => 1,
MdlFile.VertexType.Single4 => 2,
_ => throw _notifier.Exception($"Unexpected UV vertex type {type}."),
};
}
}
usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours);
var nColors = colours?.Count ?? 0;
var materialUsages = (
uvCount,
usages.ContainsKey(MdlFile.VertexUsage.Color)
nColors
);
return materialUsages switch
{
(2, true) => typeof(VertexTexture2ColorFfxiv),
(2, false) => typeof(VertexTexture2),
(1, true) => typeof(VertexTexture1ColorFfxiv),
(1, false) => typeof(VertexTexture1),
(0, true) => typeof(VertexColorFfxiv),
(0, false) => typeof(VertexEmpty),
(3, 2) => typeof(VertexTexture3Color2Ffxiv),
(3, 1) => typeof(VertexTexture3ColorFfxiv),
(3, 0) => typeof(VertexTexture3),
(2, 2) => typeof(VertexTexture2Color2Ffxiv),
(2, 1) => typeof(VertexTexture2ColorFfxiv),
(2, 0) => typeof(VertexTexture2),
(1, 2) => typeof(VertexTexture1Color2Ffxiv),
(1, 1) => typeof(VertexTexture1ColorFfxiv),
(1, 0) => typeof(VertexTexture1),
(0, 2) => typeof(VertexColor2Ffxiv),
(0, 1) => typeof(VertexColorFfxiv),
(0, 0) => typeof(VertexEmpty),
_ => throw new Exception("Unreachable."),
_ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."),
};
}
/// <summary> Build a material vertex from a vertex's attributes. </summary>
private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary<MdlFile.VertexUsage, List<object>> attributes)
{
if (_materialType == typeof(VertexEmpty))
return new VertexEmpty();
if (_materialType == typeof(VertexColorFfxiv))
return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color]));
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
if (_materialType == typeof(VertexColor2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1));
}
if (_materialType == typeof(VertexTexture1))
return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV]));
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
if (_materialType == typeof(VertexTexture1ColorFfxiv))
return new VertexTexture1ColorFfxiv(
ToVector2(attributes[MdlFile.VertexUsage.UV]),
ToVector4(attributes[MdlFile.VertexUsage.Color])
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
);
if (_materialType == typeof(VertexTexture1Color2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture1Color2Ffxiv(
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
ToVector4(color0),
ToVector4(color1)
);
}
// XIV packs two UVs into a single vec4 attribute.
if (_materialType == typeof(VertexTexture2))
{
var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]);
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
return new VertexTexture2(
new Vector2(uv.X, uv.Y),
new Vector2(uv.Z, uv.W)
@ -427,11 +464,63 @@ public class MeshExporter
if (_materialType == typeof(VertexTexture2ColorFfxiv))
{
var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]);
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
return new VertexTexture2ColorFfxiv(
new Vector2(uv.X, uv.Y),
new Vector2(uv.Z, uv.W),
ToVector4(attributes[MdlFile.VertexUsage.Color])
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
);
}
if (_materialType == typeof(VertexTexture2Color2Ffxiv))
{
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture2Color2Ffxiv(
new Vector2(uv.X, uv.Y),
new Vector2(uv.Z, uv.W),
ToVector4(color0),
ToVector4(color1)
);
}
if (_materialType == typeof(VertexTexture3))
{
// Not 100% sure about this
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
return new VertexTexture3(
new Vector2(uv0.X, uv0.Y),
new Vector2(uv0.Z, uv0.W),
new Vector2(uv1.X, uv1.Y)
);
}
if (_materialType == typeof(VertexTexture3ColorFfxiv))
{
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
return new VertexTexture3ColorFfxiv(
new Vector2(uv0.X, uv0.Y),
new Vector2(uv0.Z, uv0.W),
new Vector2(uv1.X, uv1.Y),
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
);
}
if (_materialType == typeof(VertexTexture3Color2Ffxiv))
{
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture3Color2Ffxiv(
new Vector2(uv0.X, uv0.Y),
new Vector2(uv0.Z, uv0.W),
new Vector2(uv1.X, uv1.Y),
ToVector4(color0),
ToVector4(color1)
);
}
@ -439,25 +528,20 @@ public class MeshExporter
}
/// <summary> Get the vertex skinning type for this mesh's vertex usages. </summary>
private static Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
private Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, List<MdlFile.VertexType>> usages)
{
if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices))
{
if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4)
{
return typeof(VertexJoints8);
}
else
{
return typeof(VertexJoints4);
}
return GetFirstSafe(usages, MdlFile.VertexUsage.BlendWeights) == MdlFile.VertexType.UShort4
? typeof(VertexJoints8)
: typeof(VertexJoints4);
}
return typeof(VertexEmpty);
}
/// <summary> Build a skinning vertex from a vertex's attributes. </summary>
private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary<MdlFile.VertexUsage, List<object>> attributes)
{
if (_skinningType == typeof(VertexEmpty))
return new VertexEmpty();
@ -467,8 +551,8 @@ public class MeshExporter
if (_boneIndexMap == null)
throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available.");
var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices];
var weightsData = attributes[MdlFile.VertexUsage.BlendWeights];
var indiciesData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendIndices);
var weightsData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendWeights);
var indices = ToByteArray(indiciesData);
var weights = ToFloatArray(weightsData);
@ -495,6 +579,28 @@ public class MeshExporter
throw _notifier.Exception($"Unknown skinning type {_skinningType}");
}
/// <summary> Check that the list has length 1 for any case where this is expected and return the one entry. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private T GetFirstSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
{
var list = attributes[usage];
if (list.Count != 1)
throw _notifier.Exception($"Multiple usage indices encountered for {usage}.");
return list[0];
}
/// <summary> Check that the list has length 2 for any case where this is expected and return both entries. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (T First, T Second) GetBothSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
{
var list = attributes[usage];
if (list.Count != 2)
throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2.");
return (list[0], list[1]);
}
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private static Vector2 ToVector2(object data)
=> data switch

View file

@ -1,4 +1,3 @@
using System;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.Memory;
using SharpGLTF.Schema2;
@ -11,7 +10,7 @@ Realistically, it will need to stick around until transforms/mutations are built
and there's reason to overhaul the export pipeline.
*/
public struct VertexColorFfxiv : IVertexCustom
public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
@ -20,7 +19,7 @@ public struct VertexColorFfxiv : IVertexCustom
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector4 FfxivColor;
public Vector4 FfxivColor = ffxivColor;
public int MaxColors
=> 0;
@ -33,9 +32,6 @@ public struct VertexColorFfxiv : IVertexCustom
public IEnumerable<string> CustomAttributes
=> CustomNames;
public VertexColorFfxiv(Vector4 ffxivColor)
=> FfxivColor = ffxivColor;
public void Add(in VertexMaterialDelta delta)
{ }
@ -88,7 +84,104 @@ public struct VertexColorFfxiv : IVertexCustom
}
}
public struct VertexTexture1ColorFfxiv : IVertexCustom
public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 0;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{ }
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
public Vector2 GetTexCoord(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetTexCoord(int setIndex, Vector2 coord)
{ }
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}
public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
@ -98,9 +191,9 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0;
public Vector2 TexCoord0 = texCoord0;
public Vector4 FfxivColor;
public Vector4 FfxivColor = ffxivColor;
public int MaxColors
=> 0;
@ -113,12 +206,6 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
public IEnumerable<string> CustomAttributes
=> CustomNames;
public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor)
{
TexCoord0 = texCoord0;
FfxivColor = ffxivColor;
}
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
@ -182,7 +269,119 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
}
}
public struct VertexTexture2ColorFfxiv : IVertexCustom
public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0 = texCoord0;
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 1;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
public Vector2 GetTexCoord(int index)
=> index switch
{
0 => TexCoord0,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex >= 1)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}
public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
@ -194,9 +393,9 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0;
public Vector2 TexCoord1;
public Vector4 FfxivColor;
public Vector2 TexCoord0 = texCoord0;
public Vector2 TexCoord1 = texCoord1;
public Vector4 FfxivColor = ffxivColor;
public int MaxColors
=> 0;
@ -209,13 +408,6 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
public IEnumerable<string> CustomAttributes
=> CustomNames;
public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor)
{
TexCoord0 = texCoord0;
TexCoord1 = texCoord1;
FfxivColor = ffxivColor;
}
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
@ -282,3 +474,346 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
}
}
public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0 = texCoord0;
public Vector2 TexCoord1 = texCoord1;
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 2;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
TexCoord1 += delta.TexCoord1Delta;
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
public Vector2 GetTexCoord(int index)
=> index switch
{
0 => TexCoord0,
1 => TexCoord1,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex == 1)
TexCoord1 = coord;
if (setIndex >= 2)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}
public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor)
: IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_2",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0 = texCoord0;
public Vector2 TexCoord1 = texCoord1;
public Vector2 TexCoord2 = texCoord2;
public Vector4 FfxivColor = ffxivColor;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 3;
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
TexCoord1 += delta.TexCoord1Delta;
TexCoord2 += delta.TexCoord2Delta;
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
public Vector2 GetTexCoord(int index)
=> index switch
{
0 => TexCoord0,
1 => TexCoord1,
2 => TexCoord2,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex == 1)
TexCoord1 = coord;
if (setIndex == 2)
TexCoord2 = coord;
if (setIndex >= 3)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR":
value = FfxivColor;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4)
FfxivColor = valueVector4;
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor.X,
FfxivColor.Y,
FfxivColor.Z,
FfxivColor.W,
};
if (components.Any(component => component is < 0f or > 1f))
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
}
}
public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1)
: IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0 = texCoord0;
public Vector2 TexCoord1 = texCoord1;
public Vector2 TexCoord2 = texCoord2;
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 3;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
TexCoord1 += delta.TexCoord1Delta;
TexCoord2 += delta.TexCoord2Delta;
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
public Vector2 GetTexCoord(int index)
=> index switch
{
0 => TexCoord0,
1 => TexCoord1,
2 => TexCoord2,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex == 1)
TexCoord1 = coord;
if (setIndex == 2)
TexCoord2 = coord;
if (setIndex >= 3)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}

View file

@ -1,5 +1,5 @@
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Extensions;
using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Schema2;

View file

@ -1,5 +1,5 @@
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Extensions;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Schema2;
@ -8,6 +8,9 @@ namespace Penumbra.Import.Models.Import;
public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
{
public const int BoneLimit = 128;
public const int MaterialLimit = 10;
public static MdlFile Import(ModelRoot model, IoNotifier notifier)
{
var importer = new ModelImporter(model, notifier);
@ -208,10 +211,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
if (index >= 0)
return (ushort)index;
// If there's already 4 materials, we can't add any more.
// TODO: permit, with a warning to reduce, and validation in MdlTab.
var count = _materials.Count;
if (count >= 4)
if (count >= MaterialLimit)
return 0;
_materials.Add(materialName);
@ -234,11 +236,11 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
boneIndices.Add((ushort)boneIndex);
}
if (boneIndices.Count > 64)
throw notifier.Exception("XIV does not support meshes weighted to a total of more than 64 bones.");
if (boneIndices.Count > BoneLimit)
throw notifier.Exception($"XIV does not support meshes weighted to a total of more than {BoneLimit} bones.");
var boneIndicesArray = new ushort[64];
Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count);
var boneIndicesArray = new ushort[BoneLimit];
boneIndices.CopyTo(boneIndicesArray);
var boneTableIndex = _boneTables.Count;
_boneTables.Add(new BoneTableStruct()

View file

@ -1,5 +1,5 @@
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Extensions;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import;

View file

@ -1,6 +1,6 @@
using System.Text.Json;
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Extensions;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import;

View file

@ -319,7 +319,7 @@ public class VertexAttribute
var normals = normalAccessor.AsVector3Array();
var tangents = accessors.TryGetValue("TANGENT", out var accessor)
? accessor.AsVector4Array()
? accessor.AsVector4Array().ToArray()
: CalculateTangents(accessors, indices, normals, notifier);
if (tangents == null)

View file

@ -0,0 +1,69 @@
namespace Penumbra.Import.Models;
public static class ModelExtensions
{
// https://github.com/vpenades/SharpGLTF/blob/2073cf3cd671f8ecca9667f9a8c7f04ed865d3ac/src/Shared/_Extensions.cs#L158
private const float UnitLengthThresholdVec3 = 0.00674f;
private const float UnitLengthThresholdVec4 = 0.00769f;
internal static bool _IsFinite(this float value)
{
return float.IsFinite(value);
}
internal static bool _IsFinite(this Vector2 v)
{
return v.X._IsFinite() && v.Y._IsFinite();
}
internal static bool _IsFinite(this Vector3 v)
{
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite();
}
internal static bool _IsFinite(this in Vector4 v)
{
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite();
}
internal static Boolean IsNormalized(this Vector3 normal)
{
if (!normal._IsFinite()) return false;
return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3;
}
internal static void ValidateNormal(this Vector3 normal, string msg)
{
if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length.");
}
internal static void ValidateTangent(this Vector4 tangent, string msg)
{
if (tangent.W != 1 && tangent.W != -1) throw new ArithmeticException(msg);
new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg);
}
internal static Vector3 SanitizeNormal(this Vector3 normal)
{
if (normal == Vector3.Zero) return Vector3.UnitX;
return normal.IsNormalized() ? normal : Vector3.Normalize(normal);
}
internal static bool IsValidTangent(this Vector4 tangent)
{
if (tangent.W != 1 && tangent.W != -1) return false;
return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized();
}
internal static Vector4 SanitizeTangent(this Vector4 tangent)
{
var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal();
var s = float.IsNaN(tangent.W) ? 1 : tangent.W;
return new Vector4(n, s > 0 ? 1 : -1);
}
}

View file

@ -1,6 +1,6 @@
using Dalamud.Plugin.Services;
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;
@ -63,7 +63,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
if (info.FileType is not FileType.Model)
return [];
var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1);
var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1);
return info.ObjectType switch
{
@ -79,9 +79,9 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear
=> [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)],
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)],
ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)],
ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)],
ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)],
ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)],
ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)],
_ => [],
};
}
@ -105,7 +105,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
if (targetId == EstEntry.Zero)
return [];
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)];
return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)];
}
/// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary>
@ -137,7 +137,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
var resolvedPath = info.ObjectType switch
{
ObjectType.Character => GamePaths.Character.Mtrl.Path(
ObjectType.Character => GamePaths.Mtrl.Customization(
info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant),
_ => absolutePath,
};

View file

@ -1,5 +1,5 @@
using System.Xml;
using OtterGui;
using OtterGui.Extensions;
using Penumbra.Import.Models.Export;
namespace Penumbra.Import.Models;

View file

@ -26,7 +26,7 @@ public class SimpleMod
public class ModPackPage
{
public int PageIndex = 0;
public ModGroup[] ModGroups = Array.Empty<ModGroup>();
public ModGroup[] ModGroups = [];
}
[Serializable]
@ -34,7 +34,7 @@ public class ModGroup
{
public string GroupName = string.Empty;
public GroupType SelectionType = GroupType.Single;
public OptionList[] OptionList = Array.Empty<OptionList>();
public OptionList[] OptionList = [];
public string Description = string.Empty;
}
@ -44,7 +44,7 @@ public class OptionList
public string Name = string.Empty;
public string Description = string.Empty;
public string ImagePath = string.Empty;
public SimpleMod[] ModsJsons = Array.Empty<SimpleMod>();
public SimpleMod[] ModsJsons = [];
public string GroupName = string.Empty;
public GroupType SelectionType = GroupType.Single;
public bool IsChecked = false;
@ -59,8 +59,8 @@ public class ExtendedModPack
public string Version = string.Empty;
public string Description = DefaultTexToolsData.Description;
public string Url = string.Empty;
public ModPackPage[] ModPackPages = Array.Empty<ModPackPage>();
public SimpleMod[] SimpleModsList = Array.Empty<SimpleMod>();
public ModPackPage[] ModPackPages = [];
public SimpleMod[] SimpleModsList = [];
}
[Serializable]
@ -72,5 +72,5 @@ public class SimpleModPack
public string Version = string.Empty;
public string Description = DefaultTexToolsData.Description;
public string Url = string.Empty;
public SimpleMod[] SimpleModsList = Array.Empty<SimpleMod>();
public SimpleMod[] SimpleModsList = [];
}

View file

@ -119,7 +119,7 @@ public partial class TexToolsImporter : IDisposable
// Puts out warnings if extension does not correspond to data.
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
{
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar")
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".pcp" or ".zip" or ".7z" or ".rar")
return HandleRegularArchive(modPackFile);
using var zfs = modPackFile.OpenRead();

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