Compare commits

...

922 commits

Author SHA1 Message Date
Actions User
eec8ee7094 [CI] Updating repo.json for 1.5.1.13
Some checks failed
.NET Build / build (push) Has been cancelled
2026-01-27 15:30:23 +00:00
Marc-Aurel Zent
13500264b7 Use iced to create AsmHooks in PapRewriter.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-12-22 15:31:12 +01:00
Actions User
6ba735eefb [CI] Updating repo.json for 1.5.1.12
Some checks failed
.NET Build / build (push) Has been cancelled
2025-12-20 20:53:36 +00:00
Ottermandias
73f02851a6 Cherry pick API support for other block compression types from Luna branch. 2025-12-20 21:51:40 +01:00
Actions User
069323cfb8 [CI] Updating repo.json for 1.5.1.11
Some checks are pending
.NET Build / build (push) Waiting to run
2025-12-20 14:38:27 +00:00
Ottermandias
9aa566f521 Fix typo in new IPC providers. 2025-12-20 15:36:11 +01:00
Ottermandias
eff3784a85 Fix multi-release bug in texturearrayslicer. 2025-12-20 15:36:03 +01:00
Ottermandias
9cf7030f87 ...
Some checks failed
.NET Build / build (push) Has been cancelled
2025-12-19 01:13:08 +01:00
Actions User
deb3686df5 [CI] Updating repo.json for 1.5.1.9 2025-12-19 00:12:44 +00:00
Ottermandias
953f243caf . 2025-12-19 01:08:18 +01:00
Ottermandias
59fec5db82 Needs both versions for now due to flatsharp? 2025-12-19 01:05:50 +01:00
Ottermandias
37f3044376 Update dotnet. 2025-12-19 00:56:50 +01:00
Ottermandias
fb299d71f0 Remove unimplemented ipc. 2025-12-19 00:54:09 +01:00
Ottermandias
dbcb2e38ec Merge remote-tracking branch 'Exter-N/settings-sections' 2025-12-19 00:51:45 +01:00
Ottermandias
ebcbc5d98a Update SDK. 2025-12-19 00:51:39 +01:00
Ottermandias
febced0708 Fix bug in slicer. 2025-12-19 00:47:19 +01:00
Ottermandias
4c8ff40821 Fix private Unks.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-12-18 20:47:49 +01:00
Ottermandias
7717251c6a Update to TerraFX. 2025-12-18 20:45:15 +01:00
Ottermandias
3e7511cb34 Update SDK.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-12-17 18:33:10 +01:00
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
Exter-N
338e3bc1a5 Update Penumbra.Api 2025-11-20 18:41:32 +01:00
Exter-N
e240a42a2c Replace GetPlugin(Delegate) stub by actual implementation 2025-11-13 19:55:32 +01:00
Exter-N
5be021b0eb Add integration settings sections 2025-11-13 19:53:50 +01: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
Ottermandias
d2a015f32a Ughhhhhhhhhh 2024-11-26 01:58:44 +01:00
Ottermandias
d0e0ae46e6 Push an ID in itemselector. 2024-11-25 17:46:14 +01:00
Ottermandias
9822ab4128 Add some debug helper output for SeFileDescriptor. 2024-11-25 16:59:07 +01:00
Ottermandias
25aac1a03e Fix CalculateHeight. 2024-11-23 13:58:00 +01:00
Actions User
5a46361d4f [CI] Updating repo.json for 1.3.1.2 2024-11-22 18:16:51 +00:00
Ottermandias
17d8826ae9 This time correctly, maybe? 2024-11-22 19:13:01 +01:00
Actions User
22be9f2d07 [CI] Updating repo.json for 1.3.1.1 2024-11-22 16:34:25 +00:00
Ottermandias
06ba0ba956 Fix glasses issue with resource trees. 2024-11-22 17:32:00 +01:00
Ottermandias
234130cf86
Update repo.json 2024-11-22 14:44:00 +01:00
Actions User
977cb2196a [CI] Updating repo.json for 1.3.1.0 2024-11-22 13:29:57 +00:00
Ottermandias
37332c432b 1.3.1.0 2024-11-22 12:36:30 +01:00
Ottermandias
ee48ea0166 Some stashed changes already applied. 2024-11-22 00:43:36 +01:00
Ottermandias
f2bdaf1b49 Circumvent rsf not existing. 2024-11-22 00:35:17 +01:00
Ottermandias
9e72432682 Merge branch 'master' of github.com:xivDev/Penumbra 2024-11-20 18:08:36 +01:00
Ottermandias
ce75471e51 Fix issue with resetting GEQP parameters on reload (again?) 2024-11-20 18:08:24 +01:00
Actions User
688b84141f [CI] Updating repo.json for testing_1.3.0.4 2024-11-20 09:46:01 +00:00
Ottermandias
3beef61c6f Some debug vis improvements, disable .atch file modding for the moment until modular .atch file modding is implemented. 2024-11-20 10:44:09 +01:00
Ottermandias
11d0cfd1e2 Merge branch 'master' of github.com:xivDev/Penumbra 2024-11-18 20:14:35 +01:00
Ottermandias
8a53313c33 Add .atch file debugging. 2024-11-18 20:14:06 +01:00
Actions User
0928d712c9 [CI] Updating repo.json for testing_1.3.0.3 2024-11-17 23:30:41 +00:00
Ottermandias
41718d8f8f Fix screen actor indices. 2024-11-18 00:28:42 +01:00
Ottermandias
7ab5299f7a Add SCD handling and crc cache visualization. 2024-11-18 00:28:42 +01:00
Actions User
597380355a [CI] Updating repo.json for testing_1.3.0.2 2024-11-17 21:02:11 +00:00
Ottermandias
a864ac1965 1.3.0.2 2024-11-17 22:00:07 +01:00
Ottermandias
83e5feb7db Update OtterGui. 2024-11-17 14:30:47 +01:00
Ottermandias
5599f12753 Further fixes. 2024-11-17 14:28:33 +01:00
Ottermandias
e3a1ae6938 Current state. 2024-11-17 00:50:39 +01:00
Actions User
c54141be54 [CI] Updating repo.json for testing_1.3.0.1 2024-11-04 12:59:01 +00:00
Ottermandias
7dfc564a4c Add path resolving / est handling for kdb and bnmb files. 2024-11-04 13:55:45 +01:00
Ottermandias
d50fbf5a1c Merge remote-tracking branch 'origin/master' 2024-10-30 20:48:26 +01:00
Ottermandias
ed717c69f9 Make temporary collection always respect ownership. 2024-10-30 20:48:16 +01:00
Ottermandias
c4f6038d1e Make temporary collection always respect ownership. 2024-10-30 20:40:10 +01:00
Ottermandias
2358eb378d Fix issue with characters in login screen, maybe. 2024-10-30 17:31:38 +01:00
Ottermandias
7e6ea5008c Maybe fix other issue with left rings and resource trees. 2024-10-30 17:31:24 +01:00
Ottermandias
69971c12af Fix EQP entries for earring hiding. 2024-10-19 19:23:58 +02:00
Actions User
71101ef553 [CI] Updating repo.json for 1.3.0.0 2024-10-18 14:26:25 +00:00
Ottermandias
472d803141 1.3.0.0 2024-10-18 16:24:30 +02:00
Ottermandias
9ddb011545 Fix issue with long mod titles in the merge mods tab. 2024-10-18 16:03:08 +02:00
Ottermandias
339d1f8caf Update GameData 2024-10-13 14:41:27 +02:00
Actions User
9bd1f86a1d [CI] Updating repo.json for testing_1.2.1.9 2024-10-13 11:57:07 +00:00
Ottermandias
50db83146a Maybe fix left finger resource nodes. 2024-10-13 13:55:07 +02:00
Actions User
a54e45f9c3 [CI] Updating repo.json for testing_1.2.1.8 2024-10-12 13:15:18 +00:00
Ottermandias
e646b48afa Add swaps to and from Glasses. 2024-10-12 15:13:22 +02:00
Ottermandias
97b310ca3f Fix issue with meta file not being saved synchronously on creation. 2024-10-12 15:13:06 +02:00
Ottermandias
db2ce1328f Enable VFX for the glasses slot. 2024-10-11 18:19:12 +02:00
Ottermandias
1d5a7a41ab Remove BonusItem from use and update ResourceTree a bit. 2024-10-11 16:35:47 +02:00
Ottermandias
2c5ffc1bc5 Add delete and single add button and fix child sizes. 2024-10-10 16:50:12 +02:00
Actions User
40c772a9da [CI] Updating repo.json for testing_1.2.1.7 2024-10-09 16:49:59 +00:00
Ottermandias
4a0c996ff6 Fix some off-by-one errors with the import progress reports, add test implementation for pbd editing. 2024-10-09 18:47:30 +02:00
Ottermandias
2e424a693d Update GameData 2024-10-07 16:18:58 +02:00
Actions User
c4b59295cb [CI] Updating repo.json for testing_1.2.1.6 2024-10-06 12:54:51 +00:00
Ottermandias
740816f3a6 Fix accessory VFX change not working. 2024-10-06 14:51:52 +02:00
Ottermandias
df0526e6e5 Fix readoing and displaying DemiHuman IMC Identifiers. 2024-10-06 14:23:36 +02:00
Ottermandias
76c0264cbe Reenable model IO for testing. 2024-10-06 14:05:13 +02:00
ackwell
a1a880a0f4 Fix hair.shpk 2024-10-06 14:02:56 +02:00
ackwell
3b21de35cc Fix iris.shpk 2024-10-06 14:02:56 +02:00
ackwell
efd08ae053 Add charactertattoo.shpk support 2024-10-06 14:02:56 +02:00
ackwell
8468ed2c07 Fix skin.shpk 2024-10-06 14:02:56 +02:00
ackwell
8fa0875ec6 Fix character*.shpk exports 2024-10-06 14:02:56 +02:00
Passive
4719f413b6 Fix adjustment switch 2024-10-06 14:02:56 +02:00
Passive
5258c600b7 Rework AdjustByteArray 2024-10-06 14:02:56 +02:00
Passive
3e90524b06 Remove old impl comments 2024-10-06 14:02:56 +02:00
Passive
9c6498e028 Conditionally still check for weights1 even if weights0 is 0 2024-10-06 14:02:56 +02:00
Passive
9de6b3a905 Vector4 to float array 2024-10-06 14:02:56 +02:00
Passive
fecdee05bd Cleanup 2024-10-06 14:02:56 +02:00
Passive
8084f48144 Init support for DT model i/o 2024-10-06 14:02:56 +02:00
Ottermandias
389c42e68f Update GameData. 2024-10-06 13:02:28 +02:00
Ottermandias
776b4e9efb Update obsolete properties from CS. 2024-10-06 11:58:06 +02:00
Ottermandias
caf4382e1f Update BNPCs 2024-10-06 11:58:06 +02:00
Actions User
22aca49112 [CI] Updating repo.json for testing_1.2.1.5 2024-09-22 19:47:34 +00:00
Ottermandias
af2a14826c Add potential hidden priorities. 2024-09-19 22:50:00 +02:00
Ottermandias
9b958a9d37 Update actions. 2024-09-16 23:16:43 +02:00
Ottermandias
00fbb2686b Add option to apply only attributes from IMC group. 2024-09-16 22:54:14 +02:00
Actions User
ac1ea124d9 [CI] Updating repo.json for testing_1.2.1.4 2024-09-09 14:53:17 +00:00
Ottermandias
26371d42f7 Be less dumb. 2024-09-09 16:51:29 +02:00
Actions User
10ce5da8c9 [CI] Updating repo.json for testing_1.2.1.3 2024-09-09 13:42:42 +00:00
Ottermandias
0c6d777c75 Allow copying paths out of the resource logger. 2024-09-09 14:10:54 +02:00
Ottermandias
bd59591ed8 Add display of ImportDate and allow resetting it, add button to open local data json. 2024-09-08 23:42:19 +02:00
Ottermandias
22cbecc6a4 Add Page to mod group data for TT interop. 2024-09-08 22:48:15 +02:00
Ottermandias
1b17404876 Fix small issue with changed item tooltips. 2024-08-31 20:52:08 +02:00
Ottermandias
6b858dc5ac Hmpf. 2024-08-31 20:52:08 +02:00
Ottermandias
75858a61b5 Fix MetaManipulations not resetting count when clearing. 2024-08-31 20:52:08 +02:00
Ottermandias
04582ba00b Add CustomArmor to UI events. 2024-08-31 20:52:08 +02:00
Ottermandias
fb144d0b74 Cleanup. 2024-08-31 20:52:08 +02:00
Actions User
ff3e5410aa [CI] Updating repo.json for testing_1.2.1.2 2024-08-29 19:18:17 +00:00
Ottermandias
176001195b Improve mod filters. 2024-08-29 21:13:33 +02:00
Ottermandias
2a7d2ef0d5 Allow reading BC6. 2024-08-29 18:58:30 +02:00
Ottermandias
de3644e9e1 Make BC4 textures importable. 2024-08-29 18:46:37 +02:00
Ottermandias
5c5e45114f Make loading mods for advanced editing async. 2024-08-29 18:38:37 +02:00
Ottermandias
e5ff9cee9e Stop raising errors when compressing the deleted files after updating Heliosphere mods. 2024-08-29 18:38:09 +02:00
Ottermandias
f5e6132462 Delete default meta entries from archives and api added mods if not configured otherwise. 2024-08-29 17:47:51 +02:00
Ottermandias
f8e3b6777f Add DeleteDefaultValues on general dicts. 2024-08-28 23:10:59 +02:00
Ottermandias
f043311882 Fix vulnerability warning. 2024-08-28 23:10:22 +02:00
Ottermandias
4117d45d15 Use ReadWriteDictionary as base for meta changes. 2024-08-28 18:33:01 +02:00
Ottermandias
6d408ba695 Clip meta changes. 2024-08-28 18:29:58 +02:00
Ottermandias
4970e57131 Improve tooltip of file redirections tab. 2024-08-28 18:29:12 +02:00
Ottermandias
d713d5a112 Improve handling of mod selection. 2024-08-28 18:28:49 +02:00
Ottermandias
a3c22f2826 Fix ordering of meta entries. 2024-08-28 15:49:07 +02:00
Ottermandias
233a999650 Add button to remove default-valued meta entries. 2024-08-28 15:48:42 +02:00
Ottermandias
3e2c9177a7 Prepare API for new meta format. 2024-08-28 15:48:02 +02:00
Ottermandias
ded910d8a1 Add Targa export. 2024-08-26 21:21:38 +02:00
Ottermandias
c4853434c8 Whatever. 2024-08-26 18:25:43 +02:00
Ottermandias
f3346c5d7e Add Targa support. 2024-08-26 18:20:29 +02:00
Ottermandias
726340e4f8 Meh. 2024-08-24 20:45:18 +02:00
Ottermandias
a2237773e3 Update packages. 2024-08-24 20:43:21 +02:00
Ottermandias
3549283769 Order meta entries. 2024-08-24 20:43:21 +02:00
Ottermandias
96f0479b53 Some cleanup. 2024-08-24 20:43:21 +02:00
Actions User
1da095be99 [CI] Updating repo.json for 1.2.1.1 2024-08-12 19:00:43 +00:00
Ottermandias
bedf5dab79 Make collection resolver not cache early resolved actors that aren't characters. 2024-08-12 20:58:17 +02:00
Ottermandias
bb9dd184a3 Fix order of gender and model in EQDP drawer. 2024-08-12 20:57:52 +02:00
Ottermandias
3135d5e7e6 Make collection combo mousewheel-scrollable with ctrl. 2024-08-12 20:57:38 +02:00
Ottermandias
6e351aa68b Make mods added via API migrate models if enabled. 2024-08-12 20:57:16 +02:00
Ottermandias
8ee326853d Update GameData. 2024-08-11 23:24:18 +02:00
Ottermandias
5663822b2b Merge branch 'master' of github.com:xivDev/Penumbra 2024-08-11 23:22:58 +02:00
Ottermandias
47268ab377 Prevent loading crashy shpks. 2024-08-11 23:22:42 +02:00
Ottermandias
6b0b1629bd Move GuidExtensions to OtterGui (unused atm) 2024-08-11 23:22:05 +02:00
N. Lo.
7710d92496 Add another plugin to Copy Support Info 2024-08-10 20:39:53 +02:00
Ottermandias
a27ce6b0a7 Update non-testing DalamudApiLevel. 2024-08-10 12:23:25 +02:00
Actions User
5c9e158da3 [CI] Updating repo.json for 1.2.1.0 2024-08-10 10:01:17 +00:00
Ottermandias
ccce087b87 Update GameData. 2024-08-10 11:59:18 +02:00
Ottermandias
7ba7a6e319 API 5.3 2024-08-10 11:55:30 +02:00
Ottermandias
421fde70b0 Addendum 2024-08-09 23:57:42 +02:00
Ottermandias
741141f227 Woops. 2024-08-09 23:52:12 +02:00
Ottermandias
1b671b95ab 1.2.1.0 2024-08-09 23:04:07 +02:00
Ottermandias
b5f7f03e11 Update BNPC Names. 2024-08-09 22:46:34 +02:00
Ottermandias
6c0d8ea889 Merge branch 'master' of github.com:xivDev/Penumbra 2024-08-09 22:29:04 +02:00
Ottermandias
e44b450548 Disable model import/export for now. 2024-08-09 22:17:23 +02:00
Actions User
465e65e8fe [CI] Updating repo.json for testing_1.2.0.23 2024-08-08 23:17:52 +00:00
Ottermandias
a52a43bd86 Minor cleanup. 2024-08-09 01:15:05 +02:00
Ottermandias
b3d841a8ec Merge remote-tracking branch 'Exter-N/shader-stuff' 2024-08-09 01:08:31 +02:00
Exter-N
c265b917b4 "This is how you end up on a list." 2024-08-09 01:03:46 +02:00
Exter-N
03e9dc55df Use read-only MMIO for legacy ShPk ban 2024-08-08 23:19:44 +02:00
Exter-N
fb58a9c271 Add/improve ShaderReplacementFixer hooks 2024-08-08 23:19:18 +02:00
Actions User
d630a3dff4 [CI] Updating repo.json for testing_1.2.0.22 2024-08-07 14:47:58 +00:00
Ottermandias
1648bfe424 Merge branch 'dt-shmod' 2024-08-07 16:45:28 +02:00
Ottermandias
fe4a046cc9 Make ChatWarningService part of the MessageService. 2024-08-07 16:37:58 +02:00
Ottermandias
f0c034c84d Merge branch 'master' into dt-shmod
# Conflicts:
#	Penumbra/Communication/MtrlLoaded.cs
2024-08-07 15:45:24 +02:00
Ottermandias
df58ac7e92 Fix ref. 2024-08-07 15:44:21 +02:00
Ottermandias
b8a3a854bd Merge remote-tracking branch 'Exter-N/patch-1' 2024-08-06 17:13:17 +02:00
Ottermandias
a40a5d343f Merge remote-tracking branch 'Exter-N/single-row-highlight' 2024-08-06 17:13:02 +02:00
N. Lo.
2bf08c8c89
Fix dye template combo (aka "git gud") 2024-08-06 13:05:21 +02:00
Exter-N
1187efa243 Reinstate single-row CT highlight 2024-08-05 23:02:34 +02:00
Exter-N
0d1ed6a926 No, ImGui, these buttons aren't the same. 2024-08-05 09:51:52 +02:00
N. Lo.
f68e919421 Fix LiveCTPreviewer instantiation 2024-08-05 08:41:49 +02:00
Exter-N
a36f9ccec7 Improve ResourceTree display with new function 2024-08-05 03:45:02 +02:00
Exter-N
dba85f5da3 Sanity check ShPk mods, ban incompatible ones 2024-08-05 03:43:18 +02:00
Exter-N
700fef4f04 Move hook to MaterialResourceHandle.Load (inlining my beloathed) 2024-08-05 03:41:35 +02:00
Actions User
1b5553284c [CI] Updating repo.json for testing_1.2.0.21 2024-08-04 22:44:50 +00:00
Ottermandias
2534f119e9 Make StainService deal with early-loading. 2024-08-05 00:42:04 +02:00
Ottermandias
a585976190 Make ImcChecker threadsafe. 2024-08-05 00:42:04 +02:00
Actions User
0064c4c96e [CI] Updating repo.json for testing_1.2.0.20 2024-08-04 21:23:13 +00:00
Ottermandias
e91e0b23f8 Unused usings. 2024-08-04 23:18:18 +02:00
Ottermandias
0dbf718340 Merge remote-tracking branch 'Exter-N/support-info-mdf' 2024-08-04 22:54:02 +02:00
Ottermandias
6d42673aa4 Update GameData. 2024-08-04 22:52:53 +02:00
Ottermandias
f2094c2c58 Merge branch 'dtme' 2024-08-04 22:48:41 +02:00
Ottermandias
f8b034c42d Auto-formatting, generous application of ImUtf8, minor cleanups. 2024-08-04 22:48:15 +02:00
Exter-N
f3ab1ddbb4 Add game data file status to support info 2024-08-04 22:32:31 +02:00
Ottermandias
c8e859ae05 Fixups. 2024-08-04 15:41:40 +02:00
Ottermandias
728a081419 Merge remote-tracking branch 'Exter-N/dtme' into dtme 2024-08-04 15:38:32 +02:00
Ottermandias
30d10d5a26 Merge branch 'master' into dtme
# Conflicts:
#	Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs
#	Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs
2024-08-04 15:36:17 +02:00
Ottermandias
d90c3dd1af Update row type names. 2024-08-04 15:35:27 +02:00
Exter-N
ee086e3e76 Update GameData 2024-08-04 00:57:39 +02:00
Ottermandias
a6ee4c96ea Merge branch 'rt-dt' 2024-08-04 00:00:49 +02:00
Ottermandias
da3f3b8df3 Start rework of identified objects. 2024-08-03 22:45:44 +02:00
Exter-N
75e3ef72f3 RT: Fix Facewear 2024-08-03 20:27:41 +02:00
Exter-N
243593e30f RT: Fix VPR offhand material paths 2024-08-03 20:27:38 +02:00
Exter-N
c849e31034 RT: Use SpanTextWriter to assemble paths 2024-08-03 19:48:42 +02:00
Exter-N
c01aa000fb Optimize I/O of ShPk for ResourceTree generation 2024-08-03 17:55:19 +02:00
Exter-N
5323add662 Improve ShPk tab 2024-08-03 17:55:19 +02:00
Exter-N
f4fe3605f0 DT material editor, new color tables 2024-08-03 17:55:19 +02:00
Exter-N
36ab9573ae DT material editor, main part 2024-08-03 17:55:19 +02:00
Exter-N
450751e43f DT material editor, supporting components 2024-08-03 17:55:19 +02:00
Exter-N
e8182f285e Update StainService for DT 2024-08-03 17:55:15 +02:00
Exter-N
59b3859f11 Minor upgrades to follow dependencies 2024-08-03 17:51:13 +02:00
Exter-N
60986c78f8 Update GameData 2024-08-03 17:51:11 +02:00
Exter-N
069b28272b Add TextureArraySlicer 2024-08-03 17:51:11 +02:00
Actions User
4454ac48da [CI] Updating repo.json for testing_1.2.0.19 2024-08-03 13:18:14 +00:00
Ottermandias
6241187431 Fix span constructors going over boundaries for ByteStrings. 2024-08-03 15:15:27 +02:00
Actions User
f3e7271157 [CI] Updating repo.json for testing_1.2.0.18 2024-08-02 12:48:57 +00:00
Ottermandias
d903f1b8c3 Update GetResourceSync and GetResourceAsync function signatures for testing, including unused stack parameters. 2024-08-01 22:18:26 +02:00
Ottermandias
5e9c7f7eac Fix IMC import sanity check. 2024-08-01 22:17:56 +02:00
Ottermandias
1e1637f0e7 Test IMC group toggling off. 2024-08-01 17:14:46 +02:00
Ottermandias
7579eaacbe Meh. 2024-08-01 17:14:28 +02:00
Ottermandias
73b9d1fca0 Meh. 2024-08-01 16:49:22 +02:00
Ottermandias
a308fb9f77 Allow hook overrides. 2024-08-01 16:40:37 +02:00
Ottermandias
9e15865a99 Fix some further issues with empty byte strings. 2024-08-01 16:37:31 +02:00
Ottermandias
67a220f821 Add context menu to change mod state from Collections tab. 2024-07-31 23:24:59 +02:00
Ottermandias
4b9870f090 Fix some OtterGui changes. 2024-07-31 23:09:37 +02:00
Ottermandias
d247f83e1d Use CiByteString for anything path-related. 2024-07-30 18:54:08 +02:00
Ottermandias
9d128a4d83 Fix potential threading issue on launch. 2024-07-30 18:54:08 +02:00
Ottermandias
70281c576e Update Submodules. 2024-07-30 18:54:08 +02:00
Ottermandias
5270ad4d0d Update ImageSharp 2024-07-30 18:54:08 +02:00
Ottermandias
8518240bf9 Improve knowledge window somewhat. 2024-07-30 18:54:08 +02:00
Ottermandias
7ceaeb826f Move Material Reassignment to the back (and shoot it) 2024-07-30 18:54:08 +02:00
Actions User
ee801b637e [CI] Updating repo.json for testing_1.2.0.17 2024-07-29 07:46:20 +00:00
Ottermandias
5512e0cad2 Revert no-lowercasing for the moment. 2024-07-29 09:43:15 +02:00
Ottermandias
3754f57132 Reinstate spacebar heating 2024-07-29 09:43:15 +02:00
Actions User
e52b027545 [CI] Updating repo.json for testing_1.2.0.16 2024-07-28 22:03:19 +00:00
Ottermandias
d30d418afe Remove a ToLower when resolving paths. 2024-07-28 23:56:28 +02:00
Actions User
5d50523c72 [CI] Updating repo.json for testing_1.2.0.15 2024-07-28 12:12:56 +00:00
Ottermandias
af0bbeb8bf Force saving to be synchronous. 2024-07-28 13:48:11 +02:00
Ottermandias
4806f8dc3e Do not force loaded game paths to lowercase. 2024-07-28 13:43:01 +02:00
Ottermandias
bb4665c367 Remove unused params. 2024-07-28 12:58:17 +02:00
Ottermandias
d0c4d6984c Dispose collection caches on plugin disposal. 2024-07-28 12:57:59 +02:00
Ottermandias
19166d8cf4 Add rudimentary start for knowledge window. 2024-07-28 01:11:52 +02:00
Ottermandias
a1a7487897 Remove Update Bibo Button. 2024-07-28 01:11:20 +02:00
Ottermandias
cbf5baf65c Remove Material Reassignment Tab from advanced editing due to being obsolete. 2024-07-28 01:09:48 +02:00
Ottermandias
6f3d9eb272 Fix bug in EquipmentSwap 2024-07-28 01:09:11 +02:00
Ottermandias
f143601aa0 Do not replace paths when mods are not enabled. 2024-07-28 01:08:58 +02:00
Ottermandias
3ffe6151ff Add ToString for InternalEqpEntry. 2024-07-28 01:08:41 +02:00
Ottermandias
246f4f65f5 Make items changed in a mod sort before other items for item swap, also color them. 2024-07-26 18:42:48 +02:00
Ottermandias
72f2834dfd Add some resource flags. 2024-07-24 15:11:59 +02:00
Ottermandias
c501d0b365 Fix card actor identification. 2024-07-24 15:11:59 +02:00
Actions User
30f9233862 [CI] Updating repo.json for testing_1.2.0.14 2024-07-22 18:56:35 +00:00
Ottermandias
df33557477 Cleanup. 2024-07-22 20:54:24 +02:00
Ottermandias
528e3226b5 Merge remote-tracking branch 'refs/remotes/pmgr/master' 2024-07-22 20:43:58 +02:00
Ottermandias
a4cd5695fb Fix some stuff. 2024-07-22 20:41:57 +02:00
pmgr
ca648f98a1 Fix for pap weirdness, hopefully 2024-07-22 19:37:57 +01:00
Actions User
29dce8f3ab [CI] Updating repo.json for testing_1.2.0.13 2024-07-22 13:16:22 +00:00
Ottermandias
1501bd4fbf Fix negative matching on folders with no matches. 2024-07-22 12:41:20 +02:00
Ottermandias
cec28a1823 Provide actual hook names. 2024-07-21 23:49:27 +02:00
Ottermandias
07382537a0 Merge branch 'refs/heads/pmgr/master' 2024-07-21 23:26:21 +02:00
Ottermandias
ceaa9ca29a Some further cleanup. 2024-07-21 23:26:09 +02:00
Ottermandias
ee5a21f7a2 Add pap requested event, some cleanup. 2024-07-21 22:58:24 +02:00
Ottermandias
0db70c89b1 Some cleanup of PeSigScanner. 2024-07-21 22:58:03 +02:00
Ottermandias
8351b74b21 Update GameData and packages. 2024-07-21 22:23:31 +02:00
pmgr
8c34c18643 Add scuffed pap handling 2024-07-21 16:34:27 +01:00
Actions User
48ab98bee6 [CI] Updating repo.json for testing_1.2.0.12 2024-07-20 22:53:37 +00:00
Ottermandias
c3b7ddad28 Create newly added mods in import folder instead of moving them. 2024-07-21 00:48:48 +02:00
Ottermandias
5b1c0cf0e3 Fix direction of furniture redrawing. 2024-07-21 00:21:41 +02:00
Ottermandias
e2313ba925 Fix field. 2024-07-21 00:21:27 +02:00
Ottermandias
258f7e9732 Reinstate the inlined ApricotSoundPlay hook one layer hup. 2024-07-21 00:13:43 +02:00
Ottermandias
a4548bbf04 Apply unprioritized mod groups in reverse order. 2024-07-21 00:11:13 +02:00
Ottermandias
f533ae6667 Some cleanup. 2024-07-19 17:33:54 +02:00
Ottermandias
1a8d194e05 Merge branch 'master' of github.com:xivDev/Penumbra 2024-07-19 14:24:41 +02:00
Ottermandias
f978b35b76 Make ResourceTrees work with UseNoMods. 2024-07-19 14:24:29 +02:00
Actions User
defba19b2d [CI] Updating repo.json for testing_1.2.0.11 2024-07-17 16:36:04 +00:00
Ottermandias
5abbd8b110 Hook UpdateRender despite per-frame calls. 2024-07-17 18:34:03 +02:00
Ottermandias
9bba1e2b31 Remove log spamming. 2024-07-17 18:06:05 +02:00
Actions User
6d0562180a [CI] Updating repo.json for testing_1.2.0.10 2024-07-17 16:05:28 +00:00
Ottermandias
e7c786b239 Add and rework hooks around EST entries. 2024-07-17 18:02:48 +02:00
Ottermandias
1922353ba3 Update GameData. 2024-07-17 02:01:59 +02:00
Actions User
c9379b6d60 [CI] Updating repo.json for testing_1.2.0.9 2024-07-16 23:38:11 +00:00
Ottermandias
4824a96ab0 Enable Mtrl Restore and Cleanup again. 2024-07-17 01:36:04 +02:00
Actions User
ad877e68e6 [CI] Updating repo.json for testing_1.2.0.8 2024-07-16 22:51:33 +00:00
Ottermandias
67a35b9abb stupid 2024-07-17 00:49:40 +02:00
Ottermandias
eb784dddf0 Fix missing file display. 2024-07-17 00:40:49 +02:00
Ottermandias
89cbb3f60d Update GameData. 2024-07-16 23:02:17 +02:00
Ottermandias
519b3d4891 Update GameData. 2024-07-16 22:37:01 +02:00
Actions User
78af40d507 [CI] Updating repo.json for testing_1.2.0.7 2024-07-16 20:27:30 +00:00
Ottermandias
c98bee67a5 Update GameData. 2024-07-16 22:25:28 +02:00
Ottermandias
d952d83adf Fix redrawing while fishing while sitting. 2024-07-16 22:06:09 +02:00
Ottermandias
12dfaaef99 Fix broken mods being deleted instead of removed. Fix tags crashing when null instead of empty. 2024-07-16 22:06:09 +02:00
Ottermandias
9c781f8563 Disable material migration for now 2024-07-16 22:06:09 +02:00
Actions User
07c3be641d [CI] Updating repo.json for testing_1.2.0.6 2024-07-14 18:41:39 +00:00
Ottermandias
e46fcc4af1 Gracefully deal with invalid offhand IMCs. 2024-07-14 20:38:54 +02:00
Ottermandias
d6f61f06cb Add TestingDalamudApiLevel 2024-07-12 22:15:00 +02:00
Actions User
d815266ed7 [CI] Updating repo.json for testing_1.2.0.5 2024-07-12 15:56:58 +00:00
Ottermandias
94a05afbe0 Make the import popup closeable by clicking outside if it is finished. 2024-07-12 17:54:47 +02:00
Ottermandias
22af545e8d Add image strings to groups and mods to keep them in the json on saves. 2024-07-12 17:37:55 +02:00
Ottermandias
40be298d67 Add automatic reduplication for ui files in pmps, test. 2024-07-12 17:37:19 +02:00
Ottermandias
6beb2416dc Merge branch 'master' of github.com:xivDev/Penumbra 2024-07-12 16:24:56 +02:00
Ottermandias
24597d7dc0 Fix mod normalization skipping the default submod. 2024-07-12 16:20:17 +02:00
Actions User
34cbf37c32 [CI] Updating repo.json for testing_1.2.0.4 2024-07-10 23:43:04 +00:00
Ottermandias
380dd0cffb Fix texture writing. 2024-07-11 01:40:45 +02:00
Ottermandias
1be75444cd Update BNPC Names 2024-07-10 12:51:55 +02:00
Actions User
e2112202a0 [CI] Updating repo.json for testing_1.2.0.3 2024-07-09 16:38:39 +00:00
Ottermandias
37ffe52869 Fix issue with file substitutions. 2024-07-09 18:36:45 +02:00
Ottermandias
baa439d246 Fix enable/disable draw offsets. 2024-07-09 18:31:31 +02:00
Actions User
806e001bad [CI] Updating repo.json for testing_1.2.0.2 2024-07-09 16:11:41 +00:00
Ottermandias
3b980c1a49 Fix two import bugs. 2024-07-09 18:09:44 +02:00
Actions User
1efd493834 [CI] Updating repo.json for testing_1.2.0.1 2024-07-09 15:52:40 +00:00
Ottermandias
3c417d7aec Fix extraction of pmp failing 2024-07-09 17:48:42 +02:00
Ottermandias
c2517499f2
Update repo.json 2024-07-09 17:04:27 +02:00
Actions User
56502f19f9 [CI] Updating repo.json for testing_1.2.0.0 2024-07-09 14:55:15 +00:00
Ottermandias
a0a3435918 Remove not-yet-existing CS requirement. 2024-07-09 16:50:48 +02:00
Ottermandias
b677a14cef Update. 2024-07-09 16:34:31 +02:00
Ottermandias
710f39768b Disable the required ShadersKnown for the time being. 2024-07-08 15:00:37 +02:00
Ottermandias
f89eea721f Update game data. 2024-07-08 14:59:35 +02:00
Ottermandias
585601efd4 Merge branch 'refs/heads/Exter-N/srf7'
# Conflicts:
#	Penumbra/Interop/Hooks/HookSettings.cs
2024-07-08 14:57:37 +02:00
Ottermandias
56e284a99e Add some migration things. 2024-07-08 14:55:49 +02:00
Ottermandias
0d939b12f4 Add model update button. 2024-07-07 16:17:12 +02:00
Ottermandias
4f0f3721a6 Update animation hooks. 2024-07-07 16:16:58 +02:00
Ottermandias
68135f3757 Update Gamedata 2024-07-07 16:15:51 +02:00
Exter-N
41d271213e Update ShaderReplacementFixer for 7.0 2024-07-05 23:59:22 +02:00
Ottermandias
1284037554 Fix some hooks. 2024-07-05 14:39:03 +02:00
Ottermandias
4026dd5867 Change texture handling. 2024-07-05 12:14:31 +02:00
Ottermandias
9fb8090781 Current state. 2024-07-03 17:29:49 +02:00
Ottermandias
431933e9c1 Revert repo API version. 2024-07-02 18:27:53 +02:00
Ottermandias
221b18751d Some updates. 2024-07-02 17:08:27 +02:00
Ottermandias
c2e74ed382 Improve signatures. 2024-06-25 22:50:52 +02:00
Ottermandias
b07af32dee Fix doubled hook. 2024-06-22 23:04:16 +02:00
Actions User
045abc787d [CI] Updating repo.json for testing_1.1.1.5 2024-06-20 12:53:23 +00:00
Ottermandias
ab1e11aba1 Improve support info a bit. 2024-06-20 14:51:17 +02:00
Ottermandias
8cd8efa723 Fix RSP scaling for NPC values. 2024-06-20 14:24:43 +02:00
Actions User
f686a0ff09 [CI] Updating repo.json for testing_1.1.1.4 2024-06-19 20:37:08 +00:00
Ottermandias
29f8c91306 Make meta hooks respect Enable Mod setting and fix EQP composition. 2024-06-19 22:34:59 +02:00
Ottermandias
a90e253c73
Update repo.json 2024-06-19 13:54:17 +02:00
Actions User
90124e83df [CI] Updating repo.json for testing_1.1.1.3 2024-06-19 11:51:55 +00:00
Ottermandias
819afc518c Merge branch 'meta_rework' 2024-06-18 22:05:15 +02:00
Ottermandias
e05dbe9885 Make everything services. 2024-06-18 21:59:04 +02:00
Ottermandias
cf1dcfcb7c Improve Path preprocessing. 2024-06-18 18:33:37 +02:00
Ottermandias
f9c45a2f3f Clean unused functions. 2024-06-18 17:57:12 +02:00
Ottermandias
03d3c38ad5 Improve Imc Handling. 2024-06-18 17:52:34 +02:00
Ottermandias
d7a8c9415b Use specific counter for Imc. 2024-06-17 23:11:42 +02:00
Ottermandias
be729afd4b Some cleanup 2024-06-17 16:39:10 +02:00
Ottermandias
91d9e465ed Improve eqdp. 2024-06-17 14:51:10 +02:00
Ottermandias
600fd2ecd3 Get rid off EQDP files 2024-06-17 14:51:10 +02:00
Ottermandias
9ecc4ab46d Remove CMP file. 2024-06-17 14:51:10 +02:00
Ottermandias
ebef4ff650 No EST files anymore. 2024-06-17 14:51:10 +02:00
Ottermandias
943207cae8 Make GMP independent of file, cleanup unused functions. 2024-06-17 14:51:10 +02:00
Ottermandias
c53f29c257 Fix unnecessary EST file creations. 2024-06-17 14:51:10 +02:00
Ottermandias
a7b90639c6 Some fixes. 2024-06-17 14:51:10 +02:00
Ottermandias
a61a96f1ef Make GmpEntry readonly. 2024-06-17 14:51:10 +02:00
Ottermandias
30b32fdcd2 Fix EQDP bug. 2024-06-17 14:51:10 +02:00
Ottermandias
ad0c64d4ac Change Eqp hook to not need eqp files anymore. 2024-06-17 14:51:10 +02:00
Ottermandias
e33512cf7f Fix issue, remove IMetaCache. 2024-06-17 14:51:10 +02:00
Ottermandias
4ca49598f8 Small improvement. 2024-06-17 14:51:09 +02:00
Ottermandias
3170edfeb6 Get rid off all MetaManipulation things. 2024-06-17 14:51:09 +02:00
Ottermandias
361082813b tmp 2024-06-17 14:51:09 +02:00
Ottermandias
196ca2ce39 Remove all usages of Add(MetaManipulation) 2024-06-17 14:51:09 +02:00
Ottermandias
d9b63320f0 Some small fixes, parse directly into MetaDictionary. 2024-06-17 14:51:09 +02:00
Ottermandias
0445ed0ef9 Remove TryGetValue(MetaManipulation) from MetaDictionary. 2024-06-17 14:51:09 +02:00
Ottermandias
5ca9e63a2a Use internal entries. 2024-06-17 14:51:09 +02:00
Ottermandias
e0339160e9 Start removing MetaManipulation functions. 2024-06-17 14:51:09 +02:00
Ottermandias
13156a58e9 Remove unused functions. 2024-06-17 14:51:09 +02:00
Ottermandias
94fdd848b7 Expand on MetaDictionary to use separate dictionaries. 2024-06-17 14:51:09 +02:00
Ottermandias
d7b60206d7 Improve meta manipulation handling a bit. 2024-06-17 14:51:09 +02:00
Ottermandias
250c4034e0 Improve root directory behavior and AddMods. 2024-06-17 14:50:49 +02:00
Ottermandias
b3f8762494 Fix some crash handler issues 2024-06-17 14:50:44 +02:00
Ottermandias
ec207bdba2 Force saves independent of manipulations for swaps and merges. 2024-06-15 20:49:15 +02:00
Ottermandias
b1a0590382 Make modmerger file lookup case insensitive. 2024-06-15 20:49:15 +02:00
Actions User
532e8a0936 [CI] Updating repo.json for 1.1.1.1 2024-06-14 12:11:16 +00:00
Ottermandias
2346b7588a Fix GMP bug. 2024-06-14 13:39:25 +02:00
Ottermandias
447735f609 Add a configuration to disable showing mods in the lobby and at the aesthetician. 2024-06-11 16:32:38 +02:00
Actions User
c8ea33f8dd [CI] Updating repo.json for 1.1.1.0 2024-06-11 10:52:49 +00:00
Ottermandias
863a7edf0e Add changelog. 2024-06-11 12:50:13 +02:00
Ottermandias
30a87e3f40 Update valid world check. 2024-06-11 12:28:33 +02:00
Ottermandias
ecd5752d16 Make imc attribute letter tooltip appear on disabled. 2024-06-11 12:27:33 +02:00
Ottermandias
f7adc83d63 Fix issue with preview of file in advanced model editing. 2024-06-11 12:27:22 +02:00
Ottermandias
f6b35497c5 Change path comparison for AddMod. 2024-06-11 12:27:10 +02:00
Ottermandias
f51fc2cafd Allow root directory overwriting with case sensitivity. 2024-06-11 12:27:03 +02:00
Ottermandias
159942f29c Add by-name identification in the lobby. 2024-06-11 12:26:54 +02:00
Ottermandias
e884b269a9 Add a version field to mod group files. 2024-06-11 12:26:11 +02:00
Ottermandias
de0309bfa7 Fix issue with ImcGroup settings and IPC. 2024-06-11 12:25:21 +02:00
Actions User
102e7335a7 [CI] Updating repo.json for testing_1.1.0.3 2024-06-07 14:40:25 +00:00
Ottermandias
50a7e7efb7 Add more filter options. 2024-06-07 16:34:09 +02:00
Ottermandias
2e9f184454 Introduce Identifiers and strong entry types for each meta manipulation and use them in the manipulations. 2024-06-06 17:26:25 +02:00
Ottermandias
ceed8531af Fix GMP Entry edit. 2024-06-06 16:49:39 +02:00
Ottermandias
03bfbcc309 Fidx wrong group 2024-06-05 10:36:38 +02:00
Ottermandias
afdffa4f2c Merge branch 'master' of github.com:xivDev/Penumbra 2024-06-04 15:53:43 +02:00
Ottermandias
48dd4bcadb Bleh. 2024-06-04 15:53:35 +02:00
ackwell
87fec7783e Fix blend weight adjustment getting stuck on near-bounds values 2024-06-04 09:36:46 +02:00
Ottermandias
699ae8e1fb Fix issue with collection settings being set to negative value for some reason. 2024-06-03 17:47:11 +02:00
Ottermandias
aeb2db9f5d Add tooltip to global eqp condition. 2024-06-03 17:46:45 +02:00
Ottermandias
63b3a02e95 Fix issue with crash handler and collections not saving on rename. 2024-06-03 17:45:22 +02:00
Ottermandias
b63935e81e Fix issue with accessory vfx hook. 2024-06-02 12:09:05 +02:00
Ottermandias
05d010a281 Add some functionality to allow an IMC group to add apply to all variants. 2024-06-02 12:08:49 +02:00
Ottermandias
137b752196 Fix Dye Preview not applying. 2024-06-02 01:53:29 +02:00
Ottermandias
3b81dd89c8 Merge branch 'restree-blurb' 2024-06-02 01:04:08 +02:00
Ottermandias
3deda68eec Small updates. 2024-06-02 01:03:51 +02:00
Exter-N
3b26e97231 Merge branch 'master' into restree-blurb 2024-06-02 00:05:16 +02:00
Actions User
2e6473dc09 [CI] Updating repo.json for 1.1.0.2 2024-06-01 21:44:53 +00:00
Ottermandias
ef9d81c061 Fix mod merger. 2024-06-01 23:42:17 +02:00
Ottermandias
cfa58ee196 Fix global EQP rings checking bracelets instead. 2024-06-01 23:42:04 +02:00
Ottermandias
e7cf9d35c9 Add GetChangedItems for Mods. 2024-06-01 23:33:21 +02:00
Ottermandias
5101b73fdc Fix issue with creating unnamed collections. 2024-06-01 20:28:50 +02:00
Actions User
aba68cfb92 [CI] Updating repo.json for 1.1.0.1 2024-06-01 16:18:13 +00:00
Ottermandias
331b7fbc1d Fix other options displaying the same option multiple times. 2024-06-01 18:14:21 +02:00
Ottermandias
24d4e9fac6 Fix collections not being added on creation. 2024-06-01 18:14:21 +02:00
Actions User
b79600ea14 [CI] Updating repo.json for 1.1.0.0 2024-06-01 09:54:26 +00:00
Ottermandias
ce11bec985 Use strings for global eqp. 2024-05-31 22:55:22 +02:00
Ottermandias
f61bd8bb8a Update Changelog and improve metamanipulation display in advanced editing. 2024-05-31 19:37:24 +02:00
Ottermandias
67bb95f6e6 Update submodules. 2024-05-31 17:00:40 +02:00
Ottermandias
81fdbf6ccf Small cleanup. 2024-05-31 16:59:51 +02:00
Exter-N
c7046ec006 ResourceTree: Add name/path filter 2024-05-31 01:20:50 +02:00
Exter-N
f4bdbcac53 Make Resource Trees honor Incognito Mode 2024-05-30 23:09:26 +02:00
Exter-N
a6661f15e8 Display the additional path data in ResourceTree 2024-05-30 20:46:04 +02:00
Ottermandias
b2e1bff782 Consolidate path-data encoding into a single file and make it neater. 2024-05-30 17:18:46 +02:00
Actions User
09742e2e50 [CI] Updating repo.json for testing_1.0.3.2 2024-05-28 10:56:23 +00:00
Ottermandias
8891ea0570 Fix imc identifiers setting equip slot to something where they should not. 2024-05-28 12:53:02 +02:00
Ottermandias
f5d6ac8bdb Fix Remove Assignment being visible for base and interface. 2024-05-28 12:51:28 +02:00
Ottermandias
5d1b17f96d Fix mdl imports not being savable. 2024-05-28 12:51:12 +02:00
Ottermandias
255d11974f Fix IMC Stupid. 2024-05-28 11:20:36 +02:00
Actions User
eb2a9b8109 [CI] Updating repo.json for testing_1.0.3.1 2024-05-27 22:13:55 +00:00
Ottermandias
b30de460e7 Fix ColorTable preview with legacy color tables. 2024-05-28 00:12:01 +02:00
Ottermandias
f11cefcec1 Fix best match fullpath returning broken FullPath instead of nullopt. 2024-05-28 00:12:01 +02:00
Actions User
fe266dca31 [CI] Updating repo.json for testing_1.0.3.0 2024-05-27 15:45:29 +00:00
Ottermandias
ca777ba1bf Update GameData. 2024-05-27 17:43:13 +02:00
Ottermandias
1d230050c2 Fix typo and rename geqp options. 2024-05-26 15:41:27 +02:00
Ottermandias
cd133bddbb Update Changelog. 2024-05-26 14:50:22 +02:00
Ottermandias
ed083f2a4c Add support for Global EQP Changes. 2024-05-26 13:30:35 +02:00
Ottermandias
f9527970cb Fix missing ID push for imc attributes. 2024-05-25 00:43:02 +02:00
Ottermandias
e32e314863 Add initial changelog for next release. 2024-05-24 17:51:47 +02:00
Ottermandias
bad1f45ab9 Use different hooking method for EQP entries. 2024-05-24 17:34:56 +02:00
Ottermandias
4743acf767 Make IMC handling even better. 2024-05-24 16:15:04 +02:00
Ottermandias
65627b5002 Fix a weird age-old bug apparently? 2024-05-24 16:14:48 +02:00
Ottermandias
125e5628ec Fix fuckup. 2024-05-23 23:24:37 +02:00
Ottermandias
992cdff58d Improve some IMC things. 2024-05-23 22:31:39 +02:00
Ottermandias
fca1bf9d94 Add ImcIdentifier. 2024-05-23 22:30:42 +02:00
Ottermandias
7df9ddcb99 Re-Add button to open default mod json. 2024-05-23 17:25:27 +02:00
Ottermandias
dfdd5167a8 Remove auto descriptions from newly generated option groups. 2024-05-23 17:24:42 +02:00
Ottermandias
c06d5b0871 Update for new gamedata. 2024-05-23 16:57:16 +02:00
Ottermandias
2585de8b21 Cleanup group drawing somewhat. 2024-05-21 22:01:20 +02:00
Ottermandias
e85b84dafe Add the option to omit mch offhands from changed items. 2024-05-21 18:24:21 +02:00
Ottermandias
bb56faa288 Improvements. 2024-05-20 18:28:40 +02:00
Ottermandias
df6eb3fdd2 Add some early support for IMC groups. 2024-05-16 18:30:40 +02:00
Ottermandias
d47d31b665 achieve feature parity... I think. 2024-05-14 18:10:59 +02:00
Ottermandias
32dbf419e2 Fix single group default options not applying. 2024-05-09 19:58:35 +02:00
Ottermandias
bbbf65eb4c Fix bug preventing deduplication. 2024-05-09 15:49:49 +02:00
Ottermandias
1f2f66b114 Meh. 2024-05-04 11:34:02 +02:00
Ottermandias
d8dad91e89 Oop, not yet. 2024-05-04 11:24:01 +02:00
Ottermandias
97166379a7 Merge remote-tracking branch 'ackwell/mdl-io-triage-6' 2024-05-04 11:20:59 +02:00
Ottermandias
d72008b4b9 Merge remote-tracking branch 'ackwell/mdl-io-triage-6' 2024-05-04 11:19:17 +02:00
Ottermandias
2a5df2dfb0 Create permanent backup before migrating collections. 2024-05-04 11:19:07 +02:00
ackwell
7553b5da8a Fix float imprecision on blend weights 2024-05-04 04:01:28 +10:00
Ottermandias
36fc251d5b Fix a bunch of issues. 2024-05-03 18:52:52 +02:00
ackwell
46a111d152 Fix marker 2024-05-03 22:42:28 +10:00
ackwell
9063d131ba Use validation logic for new material field 2024-05-03 21:41:35 +10:00
ackwell
078688454a Show an invalid material count 2024-05-03 21:17:54 +10:00
ackwell
c96adcf557 Prevent saving invalid files 2024-05-03 21:15:00 +10:00
ackwell
2e76148fba Ensure materials end in .mtrl 2024-05-03 21:14:40 +10:00
Ottermandias
9084b43e3e Update GameData. 2024-04-28 12:01:54 +02:00
Ottermandias
cff6172453 Move editors into folder. 2024-04-27 00:07:10 +02:00
Ottermandias
1e5ed1c414 Now that was a lot of work. 2024-04-26 18:43:45 +02:00
ackwell
616db0dcc3 Add mesh vertex element readout 2024-04-26 21:23:31 +10:00
Ottermandias
297be487b5 More cleanup on groups. 2024-04-26 10:57:09 +02:00
Ottermandias
e40c4999b6 Improve collection migration maybe. 2024-04-26 10:56:36 +02:00
Ottermandias
a72be22d3b Make sure HS image does not displace the settings entirely. 2024-04-26 10:56:06 +02:00
Ottermandias
06953c175d mooooore 2024-04-25 18:18:57 +02:00
Ottermandias
0fd14ffefc More cleanup. 2024-04-25 18:14:21 +02:00
Ottermandias
72db023804 Some cleanup. 2024-04-25 17:58:32 +02:00
ackwell
c1472d5f65 Merge remote-tracking branch 'upstream/master' into mdl-io-triage-6 2024-04-25 21:30:38 +10:00
Ottermandias
cd76c31d8c Fix stack overflow. 2024-04-24 23:41:55 +02:00
Ottermandias
514121d8c1 Reorder stuff. 2024-04-24 23:28:12 +02:00
Ottermandias
6b1743b776 This sucks so hard... 2024-04-24 23:04:04 +02:00
Ottermandias
07afbfb229 Rework options, pre-submod types. 2024-04-23 17:41:55 +02:00
Ottermandias
792a04337f Add a try-catch when scanning for mods. 2024-04-23 15:50:09 +02:00
Ottermandias
e21c9fb6d1 Fix some IPC stuff. 2024-04-23 15:11:09 +02:00
Ottermandias
b34114400f Fix Havok ANSI / UTF8 Issue. 2024-04-23 15:10:16 +02:00
Ottermandias
c276f922a5 Update API. 2024-04-22 18:22:03 +02:00
ackwell
1bc3bb17c9 Fix havok parsing for non-ANSI user paths
Also improve parsing because otter is better at c# than me
2024-04-22 23:45:30 +10:00
ackwell
cc2f72b73d Use bg/ for absolute path example 2024-04-20 20:55:04 +10:00
ackwell
11acd7d3f4 Prevent import failure when no materials are present 2024-04-20 20:53:55 +10:00
ackwell
4a6d94f0fb Avoid inclusion of zero-weighted bones in name mapping 2024-04-20 20:02:43 +10:00
Ottermandias
b99a809eba Remove OptionPriority from general option groups. 2024-04-20 11:26:12 +02:00
Ottermandias
f86f29b44a Some fixes. 2024-04-20 11:03:50 +02:00
Ottermandias
2d5afde612 Fix group priority writing. 2024-04-20 11:02:30 +02:00
Ottermandias
9f4c6767f8 Remove ISubMod. 2024-04-19 18:28:25 +02:00
Ottermandias
8fc7de64d9 Start group rework. 2024-04-19 17:55:28 +02:00
ackwell
ceb3d39a9a Normalise _FFXIV_COLOR values
Fixes xivdev/Penumbra#411
2024-04-20 01:22:03 +10:00
Ottermandias
75cfffeba7 Oops. 2024-04-19 15:51:23 +02:00
Ottermandias
624dd40d58 Handle writing. 2024-04-19 15:46:52 +02:00
Ottermandias
ef1bbb6d9d I don't know what I'm doing 2024-04-19 15:40:12 +02:00
ackwell
aeb7bd5431 Ensure materials contain at least one / 2024-04-19 00:34:08 +10:00
ackwell
dbfaf37800 Export to .glb 2024-04-18 21:47:07 +10:00
ackwell
fd1f9b95d6 Add Single2 support for UVs 2024-04-18 21:23:18 +10:00
Ottermandias
1641166d6e Disable IPC listeners by default. 2024-04-17 18:15:26 +02:00
Ottermandias
0fa62f40d7 Add Versions provider. 2024-04-17 16:57:23 +02:00
Ottermandias
aeccf2b1c6 Update Submodules. 2024-04-14 15:38:14 +02:00
Ottermandias
d4183a03c0 Fix bug with new empty collections. 2024-04-14 15:00:48 +02:00
Ottermandias
94b53ce7fa Meh. 2024-04-13 16:06:04 +02:00
Ottermandias
42ad941ec2 Add GetCollectionsByIdentifier. 2024-04-13 16:05:44 +02:00
Ottermandias
6b5321dad8 Test subscription like before but without primary constructor. 2024-04-12 14:46:26 +02:00
Ottermandias
d9bd05c9ec Fix issue with Hex viewer. 2024-04-12 14:45:25 +02:00
Ottermandias
d5ed4a38e4 Fix2? 2024-04-12 14:39:12 +02:00
Ottermandias
e4f9150c9f Fix? 2024-04-12 14:30:47 +02:00
Ottermandias
791583e183 Silence readme warnings. 2024-04-12 12:51:33 +02:00
Ottermandias
1ef9346eab Allow renaming of collection. 2024-04-12 12:42:16 +02:00
Ottermandias
ba8999914f Rework API, use Collection ID in crash handler, use collection GUIDs in more places. 2024-04-12 12:33:57 +02:00
Ottermandias
793ed4f0a7 With explicit null-termination, maybe? 2024-04-12 00:02:09 +02:00
Ottermandias
eb0e7e2f5f Update ocealots code #1. 2024-04-11 21:25:00 +02:00
ocealot
45b1c55b67 Add accessory vfxs 2024-04-11 21:06:13 +02:00
Ottermandias
c0ee80629d Allow right click to paste into filter as long as it is unfocused. 2024-04-09 16:05:55 +02:00
Ottermandias
7280c4b2f7 Selector improvements. 2024-04-09 15:20:38 +02:00
Ottermandias
21a55b95d9 Add rename mod field. 2024-04-09 15:19:44 +02:00
Ottermandias
e94cdaec46 Some more. 2024-04-05 23:19:41 +02:00
Ottermandias
b1ca073276 Turn Settings and Priority into their own types. 2024-04-05 16:35:55 +02:00
Ottermandias
77bf441e62 Update Open Settings and Main UI. 2024-04-05 15:29:19 +02:00
Ottermandias
6e7512c13e Add Punchline. 2024-04-05 14:53:30 +02:00
Ottermandias
a65009dfb0 Fix issue with merging and deduplicating. 2024-04-01 13:59:09 +02:00
Ottermandias
5cebddb0ab Update OtterGui. 2024-03-30 14:59:31 +01:00
Ottermandias
e830a6b180 Merge remote-tracking branch 'Exter-N/adv-edit-improvements' 2024-03-30 14:06:18 +01:00
Exter-N
b4b813fe5e Advanced Editing minor improvements 2024-03-29 20:05:25 +01:00
Ottermandias
8fa49137b1 Merge branch 'master' of github.com:xivDev/Penumbra 2024-03-29 14:44:25 +01:00
Ottermandias
de239578cc Fix weird exception. 2024-03-29 14:44:08 +01:00
Actions User
a39419288c [CI] Updating repo.json for testing_1.0.2.6 2024-03-28 18:36:31 +00:00
Ottermandias
47c5187ad9 Derp. 2024-03-28 19:34:30 +01:00
Ottermandias
b04cb343dd Make setting for crash handler. 2024-03-28 19:32:10 +01:00
Ottermandias
a31bdb66c8 Merge branch 'pbd-modding' 2024-03-28 18:14:36 +01:00
Ottermandias
1ba5011bfa Small changes. 2024-03-28 18:13:07 +01:00
Ottermandias
e793e7793b Fix model import resetting dirty flag. 2024-03-28 15:24:37 +01:00
Exter-N
3066bf84d5 Where did these usings even come from? 2024-03-26 02:32:33 +01:00
Exter-N
efdd5a824b Make human.pbd moddable 2024-03-26 02:29:07 +01:00
Actions User
12532dee28 [CI] Updating repo.json for testing_1.0.2.5 2024-03-22 14:17:47 +00:00
Ottermandias
4175a582b8 Add IPC Providers because I'm still a fucking moron. 2024-03-22 15:15:50 +01:00
Actions User
78a3ff177f [CI] Updating repo.json for testing_1.0.2.4 2024-03-22 13:46:45 +00:00
538 changed files with 42572 additions and 17603 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

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit 4e06921da239788331a4527aa6a2943cf0e809fe
Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf

@ -1 +1 @@
Subproject commit 8787efc8fc897dfbb4515ebbabbcd5e6f54d1b42
Subproject commit 52a3216a525592205198303df2844435e382cf87

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;
@ -24,7 +26,7 @@ public record struct VfxFuncInvokedEntry(
string InvocationType,
string CharacterName,
string CharacterAddress,
string CollectionName) : ICrashDataEntry;
Guid CollectionId) : ICrashDataEntry;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface IAnimationInvocationBufferWriter
@ -32,19 +34,19 @@ public interface IAnimationInvocationBufferWriter
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
/// <param name="collectionId"> The GUID of the associated collection. </param>
/// <param name="type"> The type of VFX func called. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, AnimationInvocationType type);
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, AnimationInvocationType type);
}
internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 64;
private const int _lineCapacity = 256;
private const int _lineCapacity = 128;
private const string _name = "Penumbra.AnimationInvocation";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, AnimationInvocationType type)
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, AnimationInvocationType type)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
@ -53,10 +55,12 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, (int)type);
accessor.Write(16, characterAddress);
var span = GetSpan(accessor, 24, 104);
var span = GetSpan(accessor, 24, 16);
collectionId.TryWriteBytes(span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 40);
WriteSpan(characterName, span);
span = GetSpan(accessor, 128);
WriteString(collectionName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
@ -68,13 +72,13 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]);
var address = BitConverter.ToUInt64(line[16..]);
var characterName = ReadString(line[24..]);
var collectionName = ReadString(line[128..]);
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]);
var address = BitConverter.ToUInt64(line[16..]);
var collectionId = new Guid(line[24..40]);
var characterName = ReadString(line[40..]);
yield return new JsonObject()
{
[nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
@ -83,7 +87,7 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation
[nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type),
[nameof(VfxFuncInvokedEntry.CharacterName)] = characterName,
[nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(VfxFuncInvokedEntry.CollectionName)] = collectionName,
[nameof(VfxFuncInvokedEntry.CollectionId)] = collectionId,
};
}
}

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;
@ -8,8 +10,8 @@ public interface ICharacterBaseBufferWriter
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName);
/// <param name="collectionId"> The GUID of the associated collection. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId);
}
/// <summary> The full crash entry for a loaded character base. </summary>
@ -19,27 +21,29 @@ public record struct CharacterLoadedEntry(
int ThreadId,
string CharacterName,
string CharacterAddress,
string CollectionName) : ICrashDataEntry;
Guid CollectionId) : ICrashDataEntry;
internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 10;
private const int _lineCapacity = 256;
private const string _name = "Penumbra.CharacterBase";
private const int _version = 1;
private const int _lineCount = 10;
private const int _lineCapacity = 128;
private const string _name = "Penumbra.CharacterBase";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName)
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 108);
var span = GetSpan(accessor, 20, 16);
collectionId.TryWriteBytes(span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 36);
WriteSpan(characterName, span);
span = GetSpan(accessor, 128);
WriteString(collectionName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
@ -48,20 +52,20 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var characterName = ReadString(line[20..]);
var collectionName = ReadString(line[128..]);
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var collectionId = new Guid(line[20..36]);
var characterName = ReadString(line[36..]);
yield return new JsonObject
{
[nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(CharacterLoadedEntry.Timestamp)] = timestamp,
[nameof(CharacterLoadedEntry.ThreadId)] = thread,
[nameof(CharacterLoadedEntry.CharacterName)] = characterName,
[nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(CharacterLoadedEntry.Timestamp)] = timestamp,
[nameof(CharacterLoadedEntry.ThreadId)] = thread,
[nameof(CharacterLoadedEntry.CharacterName)] = characterName,
[nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(CharacterLoadedEntry.CollectionName)] = collectionName,
[nameof(CharacterLoadedEntry.CollectionId)] = collectionId,
};
}
}

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;
@ -8,10 +10,10 @@ public interface IModdedFileBufferWriter
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
/// <param name="collectionId"> The GUID of the associated collection. </param>
/// <param name="requestedFileName"> The file name as requested by the game. </param>
/// <param name="actualFileName"> The actual modded file name loaded. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, ReadOnlySpan<byte> requestedFileName,
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> actualFileName);
}
@ -22,34 +24,38 @@ public record struct ModdedFileLoadedEntry(
int ThreadId,
string CharacterName,
string CharacterAddress,
string CollectionName,
Guid CollectionId,
string RequestedFileName,
string ActualFileName) : ICrashDataEntry;
internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 128;
private const int _lineCapacity = 1024;
private const string _name = "Penumbra.ModdedFile";
private const int _version = 1;
private const int _lineCount = 128;
private const int _lineCapacity = 1024;
private const string _name = "Penumbra.ModdedFile";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, ReadOnlySpan<byte> requestedFileName,
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> actualFileName)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 80);
var span = GetSpan(accessor, 20, 16);
collectionId.TryWriteBytes(span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 36, 80);
WriteSpan(characterName, span);
span = GetSpan(accessor, 92, 80);
WriteString(collectionName, span);
span = GetSpan(accessor, 172, 260);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 116, 260);
WriteSpan(requestedFileName, span);
span = GetSpan(accessor, 432);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 376);
WriteSpan(actualFileName, span);
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
@ -61,24 +67,24 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var characterName = ReadString(line[20..]);
var collectionName = ReadString(line[92..]);
var requestedFileName = ReadString(line[172..]);
var actualFileName = ReadString(line[432..]);
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var collectionId = new Guid(line[20..36]);
var characterName = ReadString(line[36..]);
var requestedFileName = ReadString(line[116..]);
var actualFileName = ReadString(line[376..]);
yield return new JsonObject()
{
[nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp,
[nameof(ModdedFileLoadedEntry.ThreadId)] = thread,
[nameof(ModdedFileLoadedEntry.CharacterName)] = characterName,
[nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(ModdedFileLoadedEntry.CollectionName)] = collectionName,
[nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp,
[nameof(ModdedFileLoadedEntry.ThreadId)] = thread,
[nameof(ModdedFileLoadedEntry.CharacterName)] = characterName,
[nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(ModdedFileLoadedEntry.CollectionId)] = collectionId,
[nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName,
[nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
[nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
};
}
}

View file

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;
@ -55,7 +57,7 @@ public class CrashData
/// <summary> The last vfx function invoked before this crash data was generated. </summary>
public VfxFuncInvokedEntry? LastVfxFuncInvoked
=> LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0];
=> LastVFXFuncsInvoked.Count == 0 ? default : LastVFXFuncsInvoked[0];
/// <summary> A collection of the last few characters loaded before this crash data was generated. </summary>
public List<CharacterLoadedEntry> LastCharactersLoaded { get; set; } = [];
@ -64,5 +66,5 @@ public class CrashData
public List<ModdedFileLoadedEntry> LastModdedFilesLoaded { get; set; } = [];
/// <summary> A collection of the last few vfx functions invoked before this crash data was generated. </summary>
public List<VfxFuncInvokedEntry> LastVfxFuncsInvoked { get; set; } = [];
public List<VfxFuncInvokedEntry> LastVFXFuncsInvoked { get; set; } = [];
}

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/14.0.1">
<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": {
"net10.0-windows7.0": {
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.2.39, )",
"resolved": "1.2.39",
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
}
}
}
}

@ -1 +1 @@
Subproject commit 66687643da2163c938575ad6949c8d0fbd03afe7
Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60

@ -1 +1 @@
Subproject commit 14e00f77d42bc677e02325660db765ef11932560
Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592

View file

@ -8,6 +8,11 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}"
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
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}"
@ -18,42 +23,76 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Ap
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
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

@ -0,0 +1,78 @@
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.Interop.PathResolving;
namespace Penumbra.Api.Api;
public class ApiHelpers(
CollectionManager collectionManager,
ObjectManager objects,
CollectionResolver collectionResolver,
ActorManager actors) : IApiService
{
/// <summary> Return the associated identifier for an object given by its index. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal ActorIdentifier AssociatedIdentifier(int gameObjectIdx)
{
if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
return ActorIdentifier.Invalid;
var ptr = objects[gameObjectIdx];
return actors.FromObject(ptr, out _, false, true, true);
}
/// <summary>
/// Return the collection associated to a current game object. If it does not exist, return the default collection.
/// If the index is invalid, returns false and the default collection.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection)
{
collection = collectionManager.Active.Default;
if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
return false;
var ptr = objects[gameObjectIdx];
var data = collectionResolver.IdentifyCollection(ptr.AsObject, false);
if (data.Valid)
collection = data.ModCollection;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown")
{
if (ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged)
Penumbra.Log.Verbose($"[{name}] Called with {args}, returned {ec}.");
else
Penumbra.Log.Debug($"[{name}] Called with {args}, returned {ec}.");
return ec;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static LazyString Args(params object[] arguments)
{
if (arguments.Length == 0)
return new LazyString(() => "no arguments");
return new LazyString(() =>
{
var sb = new StringBuilder();
for (var i = 0; i < arguments.Length / 2; ++i)
{
sb.Append(arguments[2 * i]);
sb.Append(" = ");
sb.Append(arguments[2 * i + 1]);
sb.Append(", ");
}
return sb.ToString(0, sb.Length - 2);
});
}
}

View file

@ -0,0 +1,177 @@
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.Identity.Id, c => c.Identity.Name);
public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier)
{
if (identifier.Length == 0)
return [];
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.Identity.Id, collection.Identity.Name));
else if (identifier.Length >= 8)
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.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
{
if (!collections.Storage.ById(collectionId, out var collection))
collection = ModCollection.Empty;
if (collection.HasCache)
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject());
Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
return [];
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not obtain Changed Items for {collectionId}:\n{e}");
throw;
}
}
public (Guid Id, string Name)? GetCollection(ApiCollectionType type)
{
if (!Enum.IsDefined(type))
return null;
var collection = collections.Active.ByType((CollectionType)type);
return collection == null ? null : (collection.Identity.Id, collection.Identity.Name);
}
internal (Guid Id, string Name)? GetCollection(byte type)
=> GetCollection((ApiCollectionType)type);
public (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) GetCollectionForObject(int gameObjectIdx)
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
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.Identity.Id, collection.Identity.Name));
helpers.AssociatedCollection(gameObjectIdx, out collection);
return (true, false, (collection.Identity.Id, collection.Identity.Name));
}
public Guid[] GetCollectionByName(string name)
=> 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)
{
if (!Enum.IsDefined(type))
return (PenumbraApiEc.InvalidArgument, null);
var oldCollection = collections.Active.ByType((CollectionType)type);
var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
return (PenumbraApiEc.NothingChanged, old);
if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface)
return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
collections.Active.RemoveSpecialCollection((CollectionType)type);
return (PenumbraApiEc.Success, old);
}
if (!collections.Storage.ById(collectionId.Value, out var collection))
return (PenumbraApiEc.CollectionMissing, old);
if (old == null)
{
if (!allowCreateNew)
return (PenumbraApiEc.AssignmentCreationDisallowed, old);
collections.Active.CreateSpecialCollection((CollectionType)type);
}
else if (old.Value.Item1 == collection.Identity.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
collections.Active.SetCollection(collection, (CollectionType)type);
return (PenumbraApiEc.Success, old);
}
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollectionForObject(int gameObjectIdx, Guid? collectionId,
bool allowCreateNew, bool allowDelete)
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
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.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
return (PenumbraApiEc.NothingChanged, old);
if (!allowDelete)
return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
var idx = collections.Active.Individuals.Index(id);
collections.Active.RemoveIndividualCollection(idx);
return (PenumbraApiEc.Success, old);
}
if (!collections.Storage.ById(collectionId.Value, out var collection))
return (PenumbraApiEc.CollectionMissing, old);
if (old == null)
{
if (!allowCreateNew)
return (PenumbraApiEc.AssignmentCreationDisallowed, old);
var ids = collections.Active.Individuals.GetGroup(id);
collections.Active.CreateIndividualCollection(ids);
}
else if (old.Value.Item1 == collection.Identity.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
collections.Active.SetCollection(collection, CollectionType.Individual, collections.Active.Individuals.Index(id));
return (PenumbraApiEc.Success, old);
}
}

View file

@ -0,0 +1,54 @@
using OtterGui.Services;
using Penumbra.Import.Textures;
using TextureType = Penumbra.Api.Enums.TextureType;
namespace Penumbra.Api.Api;
public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IApiService
{
public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps)
=> textureType switch
{
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
TextureType.Targa => textureManager.SaveTga(inputFile, outputFile),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile),
TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile),
TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile),
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile),
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile),
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile),
TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, inputFile, outputFile),
TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, inputFile, outputFile),
TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, inputFile, outputFile),
TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, inputFile, outputFile),
TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, inputFile, outputFile),
TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, inputFile, outputFile),
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
};
// @formatter:off
public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps)
=> textureType switch
{
TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Targa => textureManager.SaveTga(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
};
// @formatter:on
}

View file

@ -0,0 +1,123 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Api.Api;
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, 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);
}
public unsafe void Dispose()
{
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
_resourceLoader.PapRequested -= OnPapRequested;
_communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
}
public event CreatedCharacterBaseDelegate? CreatedCharacterBase;
public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved;
public event CreatingCharacterBaseDelegate? CreatingCharacterBase
{
add
{
if (value == null)
return;
_communicator.CreatingCharacterBase.Subscribe(new Action<nint, Guid, nint, nint, nint>(value),
Communication.CreatingCharacterBase.Priority.Api);
}
remove
{
if (value == null)
return;
_communicator.CreatingCharacterBase.Unsubscribe(new Action<nint, Guid, nint, nint, nint>(value));
}
}
public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject)
{
var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
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
: PenumbraApiEc.InvalidArgument;
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
{
if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
{
var original = originalPath.ToString();
GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
manipulatedPath?.ToString() ?? original);
}
}
private void OnPapRequested(Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
{
if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
{
var original = originalPath.ToString();
GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
manipulatedPath?.ToString() ?? original);
}
}
private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint 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;
}

544
Penumbra/Api/Api/MetaApi.cs Normal file
View file

@ -0,0 +1,544 @@
using Dalamud.Plugin.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
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;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Api.Api;
public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers)
: IPenumbraApiMeta, IApiService
{
public string GetPlayerMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
return CompressMetaManipulations(collection);
}
public string GetMetaManipulations(int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
return CompressMetaManipulations(collection);
}
public Task<string> GetPlayerMetaManipulationsAsync()
{
return Task.Run(async () =>
{
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
return CompressMetaManipulations(playerCollection);
});
}
public Task<string> GetMetaManipulationsAsync(int gameObjectIdx)
{
return Task.Run(async () =>
{
var playerCollection = await framework.RunOnFrameworkThread(() =>
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
return collection;
}).ConfigureAwait(false);
return CompressMetaManipulations(playerCollection);
});
}
internal static string CompressMetaManipulations(ModCollection collection)
=> CompressMetaManipulationsV1(collection);
private static string CompressMetaManipulationsV0(ModCollection collection)
{
var array = new JArray();
if (collection.MetaCache is { } cache)
{
MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key));
MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair<ImcIdentifier, ImcEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair<EqpIdentifier, EqpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair<EqdpIdentifier, EqdpEntry>(kvp.Key, kvp.Value.Entry)));
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);
}
private static unsafe string CompressMetaManipulationsV1(ModCollection? collection)
{
using var ms = new MemoryStream();
ms.Capacity = 1024;
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
{
zipStream.Write((byte)1);
zipStream.Write("META0001"u8);
if (collection?.MetaCache is not { } cache)
{
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
}
else
{
WriteCache(zipStream, cache.Imc);
WriteCache(zipStream, cache.Eqp);
WriteCache(zipStream, cache.Eqdp);
WriteCache(zipStream, cache.Est);
WriteCache(zipStream, cache.Rsp);
WriteCache(zipStream, cache.Gmp);
cache.GlobalEqp.EnterReadLock();
try
{
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);
WriteCache(zipStream, cache.Shp);
WriteCache(zipStream, cache.Atr);
}
}
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)
where TKey : unmanaged, IMetaIdentifier
where TValue : unmanaged
{
metaCache.EnterReadLock();
try
{
stream.Write(metaCache.Count);
foreach (var (identifier, (_, value)) in metaCache)
{
stream.Write(identifier);
stream.Write(value);
}
}
finally
{
metaCache.ExitReadLock();
}
}
}
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, out byte version)
{
if (manipString.Length == 0)
{
manips = new MetaDictionary();
version = byte.MaxValue;
return true;
}
try
{
var bytes = Convert.FromBase64String(manipString);
using var compressedStream = new MemoryStream(bytes);
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
using var resultStream = new MemoryStream();
zipStream.CopyTo(resultStream);
resultStream.Flush();
resultStream.Position = 0;
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;
return false;
}
}
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)
{
if (!data.StartsWith("META0001"u8))
{
Penumbra.Log.Debug($"Invalid manipulations of version 1, does not start with valid prefix.");
manips = null;
return false;
}
manips = new MetaDictionary();
var r = new SpanBinaryReader(data[8..]);
var imcCount = r.ReadInt32();
for (var i = 0; i < imcCount; ++i)
{
var identifier = r.Read<ImcIdentifier>();
var value = r.Read<ImcEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var eqpCount = r.ReadInt32();
for (var i = 0; i < eqpCount; ++i)
{
var identifier = r.Read<EqpIdentifier>();
var value = r.Read<EqpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var eqdpCount = r.ReadInt32();
for (var i = 0; i < eqdpCount; ++i)
{
var identifier = r.Read<EqdpIdentifier>();
var value = r.Read<EqdpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var estCount = r.ReadInt32();
for (var i = 0; i < estCount; ++i)
{
var identifier = r.Read<EstIdentifier>();
var value = r.Read<EstEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var rspCount = r.ReadInt32();
for (var i = 0; i < rspCount; ++i)
{
var identifier = r.Read<RspIdentifier>();
var value = r.Read<RspEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var gmpCount = r.ReadInt32();
for (var i = 0; i < gmpCount; ++i)
{
var identifier = r.Read<GmpIdentifier>();
var value = r.Read<GmpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var globalEqpCount = r.ReadInt32();
for (var i = 0; i < globalEqpCount; ++i)
{
var manip = r.Read<GlobalEqpManipulation>();
if (!manip.Validate() || !manips.TryAdd(manip))
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;
}
private static bool ConvertManipsV0(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
var json = Encoding.UTF8.GetString(data);
manips = JsonConvert.DeserializeObject<MetaDictionary>(json);
return manips != null;
}
internal void TestMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
var dict = new MetaDictionary(collection.MetaCache);
var count = dict.Count;
var watch = Stopwatch.StartNew();
var v0 = CompressMetaManipulationsV0(collection);
var v0Time = watch.ElapsedMilliseconds;
watch.Restart();
var v1 = CompressMetaManipulationsV1(collection);
var v1Time = watch.ElapsedMilliseconds;
watch.Restart();
var v1Success = ConvertManips(v1, out var v1Roundtrip, out _);
var v1RoundtripTime = watch.ElapsedMilliseconds;
watch.Restart();
var v0Success = ConvertManips(v0, out var v0Roundtrip, out _);
var v0RoundtripTime = watch.ElapsedMilliseconds;
Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal");
Penumbra.Log.Information(
$"0 | {count} | {v0Time} | {v0.Length} | {v0Success} | {v0Roundtrip?.Count} | {v0RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
Penumbra.Log.Information(
$"1 | {count} | {v1Time} | {v1.Length} | {v1Success} | {v1Roundtrip?.Count} | {v1RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
}
}

View file

@ -0,0 +1,362 @@
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Interop.PathResolving;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Api.Api;
public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
{
private readonly CollectionResolver _collectionResolver;
private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager;
private readonly CollectionEditor _collectionEditor;
private readonly CommunicatorService _communicator;
public ModSettingsApi(CollectionResolver collectionResolver,
ModManager modManager,
CollectionManager collectionManager,
CollectionEditor collectionEditor,
CommunicatorService communicator)
{
_collectionResolver = collectionResolver;
_modManager = modManager;
_collectionManager = collectionManager;
_collectionEditor = collectionEditor;
_communicator = communicator;
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings);
_communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api);
_communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api);
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api);
}
public void Dispose()
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited);
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
}
public event ModSettingChangedDelegate? ModSettingChanged;
public AvailableModSettings? GetAvailableModSettings(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return null;
var dict = new Dictionary<string, (string[], int)>(mod.Groups.Count);
foreach (var g in mod.Groups)
dict.Add(g.Name, (g.Options.Select(o => o.Name).ToArray(), (int)g.Type));
return new AvailableModSettings(dict);
}
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 GetSettingsInAllCollections(string modDirectory, string modName,
out Dictionary<Guid, (bool, int, Dictionary<string, List<string>>, bool, bool)> settings,
bool ignoreTemporaryCollections = false)
{
settings = [];
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return PenumbraApiEc.ModMissing;
var collections = ignoreTemporaryCollections
? _collectionManager.Storage.Where(c => c != ModCollection.Empty)
: _collectionManager.Storage.Where(c => c != ModCollection.Empty).Concat(_collectionManager.Temp.Values);
settings = [];
foreach (var collection in collections)
{
if (GetCurrentSettings(collection, mod, false, false, 0) is { } s)
settings.Add(collection.Identity.Id, s);
}
return PenumbraApiEc.Success;
}
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);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return (PenumbraApiEc.CollectionMissing, null);
if (collection.Identity.Id == Guid.Empty)
return (PenumbraApiEc.Success, 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)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit",
inherit.ToString());
if (collectionId == Guid.Empty)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var ret = _collectionEditor.SetModInheritance(collection, mod, inherit)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetMod(Guid collectionId, string modDirectory, string modName, bool enabled)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var ret = _collectionEditor.SetModState(collection, mod, enabled)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetModPriority(Guid collectionId, string modDirectory, string modName, int priority)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Priority", priority);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var ret = _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority))
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetModSetting(Guid collectionId, string modDirectory, string modName, string optionGroupName, string optionName)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
optionGroupName, "OptionName", optionName);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
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 optionIdx = mod.Groups[groupIdx].Options.IndexOf(o => o.Name == optionName);
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
var setting = mod.Groups[groupIdx].Behaviour switch
{
GroupDrawBehaviour.MultiSelection => Setting.Multi(optionIdx),
GroupDrawBehaviour.SingleSelection => Setting.Single(optionIdx),
_ => Setting.Zero,
};
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc TrySetModSettings(Guid collectionId, string modDirectory, string modName, string optionGroupName,
IReadOnlyList<string> optionNames)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
optionGroupName, "#optionNames", optionNames.Count);
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
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
: PenumbraApiEc.NothingChanged;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc CopyModSettings(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo)
{
var args = ApiHelpers.Args("CollectionId", collectionId.HasValue ? collectionId.Value.ToString() : "NULL",
"From", modDirectoryFrom, "To", modDirectoryTo);
var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase));
var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase));
if (collectionId == null)
foreach (var collection in _collectionManager.Storage)
_collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
else if (_collectionManager.Storage.ById(collectionId.Value, out var collection))
_collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
else
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
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.GetActualSettings(mod.Index);
if (settings is { Enabled: true })
ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection);
}
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
{
if (type == ModPathChangeType.Reloaded)
TriggerSettingEdited(mod);
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool 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)
{
switch (type)
{
case ModOptionChangeType.GroupDeleted:
case ModOptionChangeType.GroupMoved:
case ModOptionChangeType.GroupTypeChanged:
case ModOptionChangeType.PriorityChanged:
case ModOptionChangeType.OptionDeleted:
case ModOptionChangeType.OptionMoved:
case ModOptionChangeType.OptionFilesChanged:
case ModOptionChangeType.OptionFilesAdded:
case ModOptionChangeType.OptionSwapsChanged:
case ModOptionChangeType.OptionMetaChanged:
TriggerSettingEdited(mod);
break;
}
}
private void OnModFileChanged(Mod mod, FileRegistry file)
{
if (file.CurrentUsage == 0)
return;
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;
}
}

165
Penumbra/Api/Api/ModsApi.cs Normal file
View file

@ -0,0 +1,165 @@
using Newtonsoft.Json.Linq;
using OtterGui.Compression;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Api.Api;
public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly ModManager _modManager;
private readonly ModImportManager _modImportManager;
private readonly Configuration _config;
private readonly ModFileSystem _modFileSystem;
private readonly MigrationManager _migrationManager;
public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem,
CommunicatorService communicator, MigrationManager migrationManager)
{
_modManager = modManager;
_modImportManager = modImportManager;
_config = config;
_modFileSystem = modFileSystem;
_communicator = communicator;
_migrationManager = migrationManager;
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods);
}
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
{
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.Moved when newDirectory != null && oldDirectory != null:
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
break;
}
}
public void Dispose()
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
}
public Dictionary<string, string> GetModList()
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
public PenumbraApiEc InstallMod(string modFilePackagePath)
{
if (!File.Exists(modFilePackagePath))
return ApiHelpers.Return(PenumbraApiEc.FileMissing, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
_modImportManager.AddUnpack(modFilePackagePath);
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
}
public PenumbraApiEc ReloadMod(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
_modManager.ReloadMod(mod);
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
}
public PenumbraApiEc AddMod(string modDirectory)
{
var args = ApiHelpers.Args("ModDirectory", modDirectory);
var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory)));
if (!dir.Exists)
return ApiHelpers.Return(PenumbraApiEc.FileMissing, args);
if (dir.Parent == null
|| Path.TrimEndingDirectorySeparator(Path.GetFullPath(_modManager.BasePath.FullName))
!= Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
_modManager.AddMod(dir, true);
if (_config.MigrateImportedModelsToV6)
{
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
_migrationManager.Await();
}
if (_config.UseFileSystemCompression)
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
CompressionAlgorithm.Xpress8K, false);
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
public PenumbraApiEc DeleteMod(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.NothingChanged, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
_modManager.DeleteMod(mod);
return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
}
public event Action<string>? ModDeleted;
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.TryGetValue(mod, out var leaf))
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
var fullPath = leaf.FullName();
var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath);
var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name);
return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault);
}
public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath)
{
if (newPath.Length == 0)
return PenumbraApiEc.InvalidArgument;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.TryGetValue(mod, out var leaf))
return PenumbraApiEc.ModMissing;
try
{
_modFileSystem.RenameAndMove(leaf, newPath);
return PenumbraApiEc.Success;
}
catch
{
return PenumbraApiEc.PathRenameFailed;
}
}
public Dictionary<string, object?> GetChangedItems(string modDirectory, string modName)
=> _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

@ -0,0 +1,43 @@
using OtterGui.Services;
namespace Penumbra.Api.Api;
public class PenumbraApi(
CollectionApi collection,
EditingApi editing,
GameStateApi gameState,
MetaApi meta,
ModsApi mods,
ModSettingsApi modSettings,
PluginStateApi pluginState,
RedrawApi redraw,
ResolveApi resolve,
ResourceTreeApi resourceTree,
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
=> (BreakingVersion, FeatureVersion);
public bool Valid { get; private set; } = true;
public IPenumbraApiCollection Collection { get; } = collection;
public IPenumbraApiEditing Editing { get; } = editing;
public IPenumbraApiGameState GameState { get; } = gameState;
public IPenumbraApiMeta Meta { get; } = meta;
public IPenumbraApiMods Mods { get; } = mods;
public IPenumbraApiModSettings ModSettings { get; } = modSettings;
public IPenumbraApiPluginState PluginState { get; } = pluginState;
public IPenumbraApiRedraw Redraw { get; } = redraw;
public IPenumbraApiResolve Resolve { get; } = resolve;
public IPenumbraApiResourceTree ResourceTree { get; } = resourceTree;
public IPenumbraApiTemporary Temporary { get; } = temporary;
public IPenumbraApiUi Ui { get; } = ui;
}

View file

@ -0,0 +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(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService
{
public string GetModDirectory()
=> config.ModDirectory;
public string GetConfiguration()
=> 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!);
}
public bool GetEnabledState()
=> config.EnableMods;
public event Action<bool>? EnabledChange
{
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

@ -0,0 +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, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
{
public void RedrawObject(int gameObjectIndex, RedrawType setting)
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting));
}
public void RedrawObject(string name, RedrawType setting)
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting));
}
public void RedrawObject(IGameObject? gameObject, RedrawType setting)
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting));
}
public void RedrawAll(RedrawType 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

@ -0,0 +1,135 @@
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Interop.PathResolving;
using Penumbra.Mods.Manager;
using Penumbra.String.Classes;
namespace Penumbra.Api.Api;
public class ResolveApi(
ModManager modManager,
CollectionManager collectionManager,
Configuration config,
CollectionResolver collectionResolver,
ApiHelpers helpers,
IFramework framework) : IPenumbraApiResolve, IApiService
{
public string ResolveDefaultPath(string gamePath)
=> ResolvePath(gamePath, modManager, collectionManager.Active.Default);
public string ResolveInterfacePath(string gamePath)
=> ResolvePath(gamePath, modManager, collectionManager.Active.Interface);
public string ResolveGameObjectPath(string gamePath, int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
return ResolvePath(gamePath, modManager, collection);
}
public string ResolvePlayerPath(string gamePath)
=> ResolvePath(gamePath, modManager, collectionResolver.PlayerCollection());
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIdx)
{
if (!config.EnableMods)
return [moddedPath];
helpers.AssociatedCollection(gameObjectIdx, out var collection);
var ret = collection.ReverseResolvePath(new FullPath(moddedPath));
return ret.Select(r => r.ToString()).ToArray();
}
public PenumbraApiEc ResolvePath(Guid collectionId, string gamePath, out string resolvedPath)
{
resolvedPath = gamePath;
if (!collectionManager.Storage.ById(collectionId, out var collection))
return PenumbraApiEc.CollectionMissing;
if (!collection.HasCache)
return PenumbraApiEc.CollectionInactive;
resolvedPath = ResolvePath(gamePath, modManager, collection);
return PenumbraApiEc.Success;
}
public string[] ReverseResolvePlayerPath(string moddedPath)
{
if (!config.EnableMods)
return [moddedPath];
var ret = collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(moddedPath));
return ret.Select(r => r.ToString()).ToArray();
}
public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse)
{
if (!config.EnableMods)
return (forward, reverse.Select(p => new[]
{
p,
}).ToArray());
var playerCollection = collectionResolver.PlayerCollection();
var resolved = forward.Select(p => ResolvePath(p, modManager, playerCollection)).ToArray();
var reverseResolved = playerCollection.ReverseResolvePaths(reverse);
return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
}
public PenumbraApiEc ResolvePaths(Guid collectionId, string[] forward, string[] reverse, out string[] resolvedForward,
out string[][] resolvedReverse)
{
resolvedForward = forward;
resolvedReverse = [];
if (!config.EnableMods)
return PenumbraApiEc.Success;
if (!collectionManager.Storage.ById(collectionId, out var collection))
return PenumbraApiEc.CollectionMissing;
if (!collection.HasCache)
return PenumbraApiEc.CollectionInactive;
resolvedForward = forward.Select(p => ResolvePath(p, modManager, collection)).ToArray();
var reverseResolved = collection.ReverseResolvePaths(reverse);
resolvedReverse = reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
return PenumbraApiEc.Success;
}
public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse)
{
if (!config.EnableMods)
return (forward, reverse.Select(p => new[]
{
p,
}).ToArray());
return await Task.Run(async () =>
{
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
var forwardTask = Task.Run(() =>
{
var forwardRet = new string[forward.Length];
Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], modManager, playerCollection));
return forwardRet;
}).ConfigureAwait(false);
var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false);
var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
return (await forwardTask, reverseResolved);
}).ConfigureAwait(false);
}
/// <summary> Resolve a path given by string for a specific collection. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private string ResolvePath(string path, ModManager _, ModCollection collection)
{
if (!config.EnableMods)
return path;
var gamePath = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty;
var ret = collection.ResolvePath(gamePath);
return ret?.ToString() ?? path;
}
}

View file

@ -0,0 +1,63 @@
using Dalamud.Game.ClientState.Objects.Types;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Interop;
using Penumbra.Interop.ResourceTree;
namespace Penumbra.Api.Api;
public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectManager objects) : IPenumbraApiResourceTree, IApiService
{
public Dictionary<string, HashSet<string>>?[] GetGameObjectResourcePaths(params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0);
var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj));
}
public Dictionary<ushort, Dictionary<string, HashSet<string>>> GetPlayerResourcePaths()
{
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly);
return ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
}
public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData,
params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj));
}
public Dictionary<ushort, GameResourceDict> GetPlayerResourcesOfType(ResourceType type,
bool withUiData)
{
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
| (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
return ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
}
public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj));
}
public Dictionary<ushort, JObject> GetPlayerResourceTrees(bool withUiData)
{
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
| (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
return resDictionary;
}
}

View file

@ -0,0 +1,338 @@
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;
namespace Penumbra.Api.Api;
public class TemporaryApi(
TempCollectionManager tempCollections,
ObjectManager objects,
ActorManager actors,
CollectionManager collectionManager,
TempModManager tempMods,
ApiHelpers apiHelpers,
ModManager modManager) : IPenumbraApiTemporary, IApiService
{
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)
? PenumbraApiEc.Success
: PenumbraApiEc.CollectionMissing;
public PenumbraApiEc AssignTemporaryCollection(Guid collectionId, int actorIndex, bool forceAssignment)
{
var args = ApiHelpers.Args("CollectionId", collectionId, "ActorIndex", actorIndex, "Forced", forceAssignment);
if (actorIndex < 0 || actorIndex >= objects.TotalCount)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
var identifier = actors.FromObject(objects[actorIndex], out _, false, false, true);
if (!identifier.IsValid)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
if (!tempCollections.CollectionById(collectionId, out var collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (forceAssignment)
{
if (tempCollections.Collections.ContainsKey(identifier) && !tempCollections.Collections.Delete(identifier))
return ApiHelpers.Return(PenumbraApiEc.AssignmentDeletionFailed, args);
}
else if (tempCollections.Collections.ContainsKey(identifier)
|| collectionManager.Active.Individuals.ContainsKey(identifier))
{
return ApiHelpers.Return(PenumbraApiEc.CharacterCollectionExists, args);
}
var group = tempCollections.Collections.GetGroup(identifier);
var ret = tempCollections.AddIdentifier(collection, group)
? PenumbraApiEc.Success
: PenumbraApiEc.UnknownError;
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary<string, string> paths, string manipString, int priority)
{
var args = ApiHelpers.Args("Tag", tag, "#Paths", paths.Count, "ManipString", manipString, "Priority", priority);
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
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
{
RedirectResult.Success => PenumbraApiEc.Success,
_ => PenumbraApiEc.UnknownError,
};
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc AddTemporaryMod(string tag, Guid collectionId, Dictionary<string, string> paths, string manipString, int priority)
{
var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "#Paths", paths.Count, "ManipString",
manipString, "Priority", priority);
if (collectionId == Guid.Empty)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
if (!tempCollections.CollectionById(collectionId, out var collection)
&& !collectionManager.Storage.ById(collectionId, out collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
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
{
RedirectResult.Success => PenumbraApiEc.Success,
_ => PenumbraApiEc.UnknownError,
};
return ApiHelpers.Return(ret, args);
}
public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority)
{
var ret = tempMods.Unregister(tag, null, new ModPriority(priority)) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
_ => PenumbraApiEc.UnknownError,
};
return ApiHelpers.Return(ret, ApiHelpers.Args("Tag", tag, "Priority", priority));
}
public PenumbraApiEc RemoveTemporaryMod(string tag, Guid collectionId, int priority)
{
var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "Priority", priority);
if (!tempCollections.CollectionById(collectionId, out var collection)
&& !collectionManager.Storage.ById(collectionId, out collection))
return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
var ret = tempMods.Unregister(tag, collection, new ModPriority(priority)) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
_ => PenumbraApiEc.UnknownError,
};
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.
/// </summary>
private static bool ConvertPaths(IReadOnlyDictionary<string, string> redirections,
[NotNullWhen(true)] out Dictionary<Utf8GamePath, FullPath>? paths)
{
paths = new Dictionary<Utf8GamePath, FullPath>(redirections.Count);
foreach (var (gString, fString) in redirections)
{
if (!Utf8GamePath.FromString(gString, out var path))
{
paths = null;
return false;
}
var fullPath = new FullPath(fString);
if (!paths.TryAdd(path, fullPath))
{
paths = null;
return false;
}
}
return true;
}
}

113
Penumbra/Api/Api/UiApi.cs Normal file
View file

@ -0,0 +1,113 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Integration;
using Penumbra.UI.Tabs;
namespace Penumbra.Api.Api;
public class UiApi : IPenumbraApiUi, IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly ConfigWindow _configWindow;
private readonly ModManager _modManager;
private readonly IntegrationSettingsRegistry _integrationSettings;
public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager, IntegrationSettingsRegistry integrationSettings)
{
_communicator = communicator;
_configWindow = configWindow;
_modManager = modManager;
_integrationSettings = integrationSettings;
_communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
_communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
}
public void Dispose()
{
_communicator.ChangedItemHover.Unsubscribe(OnChangedItemHover);
_communicator.ChangedItemClick.Unsubscribe(OnChangedItemClick);
}
public event Action<ChangedItemType, uint>? ChangedItemTooltip;
public event Action<MouseButton, ChangedItemType, uint>? ChangedItemClicked;
public event Action<string, float, float>? PreSettingsTabBarDraw
{
add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default);
remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!);
}
public event Action<string>? PreSettingsPanelDraw
{
add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default);
remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!);
}
public event Action<string>? PostEnabledDraw
{
add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default);
remove => _communicator.PostEnabledDraw.Unsubscribe(value!);
}
public event Action<string>? PostSettingsPanelDraw
{
add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default);
remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!);
}
public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName)
{
_configWindow.IsOpen = true;
if (!Enum.IsDefined(tab))
return PenumbraApiEc.InvalidArgument;
if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0))
{
if (_modManager.TryGetMod(modDirectory, modName, out var mod))
_communicator.SelectTab.Invoke(tab, mod);
else
return PenumbraApiEc.ModMissing;
}
else if (tab != TabType.None)
{
_communicator.SelectTab.Invoke(tab, null);
}
return PenumbraApiEc.Success;
}
public void CloseMainWindow()
=> _configWindow.IsOpen = false;
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data)
{
if (ChangedItemClicked == null)
return;
var (type, id) = data.ToApiObject();
ChangedItemClicked.Invoke(button, type, id);
}
private void OnChangedItemHover(IIdentifiedObjectData data)
{
if (ChangedItemTooltip == null)
return;
var (type, id) = data.ToApiObject();
ChangedItemTooltip.Invoke(type, id);
}
public PenumbraApiEc RegisterSettingsSection(Action draw)
=> _integrationSettings.RegisterSection(draw);
public PenumbraApiEc UnregisterSettingsSection(Action draw)
=> _integrationSettings.UnregisterSection(draw)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
}

View file

@ -1,17 +1,19 @@
using Dalamud.Interface;
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Api;
public class DalamudSubstitutionProvider : IDisposable
public class DalamudSubstitutionProvider : IDisposable, IApiService
{
private readonly ITextureSubstitutionProvider _substitution;
private readonly IUiBuilder _uiBuilder;
private readonly ActiveCollectionData _activeCollectionData;
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
@ -20,9 +22,10 @@ public class DalamudSubstitutionProvider : IDisposable
=> _config.UseDalamudUiTextureRedirection;
public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData,
Configuration config, CommunicatorService communicator)
Configuration config, CommunicatorService communicator, IUiBuilder ui)
{
_substitution = substitution;
_uiBuilder = ui;
_activeCollectionData = activeCollectionData;
_config = config;
_communicator = communicator;
@ -40,6 +43,9 @@ public class DalamudSubstitutionProvider : IDisposable
public void ResetSubstitutions(IEnumerable<Utf8GamePath> paths)
{
if (!_uiBuilder.UiPrepared)
return;
var transformed = paths
.Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8))
.Select(p => p.ToString());
@ -90,10 +96,7 @@ public class DalamudSubstitutionProvider : IDisposable
case ResolvedFileChanged.Type.Added:
case ResolvedFileChanged.Type.Removed:
case ResolvedFileChanged.Type.Replaced:
ResetSubstitutions(new[]
{
key,
});
ResetSubstitutions([key]);
break;
case ResolvedFileChanged.Type.FullRecomputeStart:
case ResolvedFileChanged.Type.FullRecomputeFinished:
@ -126,7 +129,7 @@ public class DalamudSubstitutionProvider : IDisposable
try
{
if (!Utf8GamePath.FromString(path, out var utf8Path, true))
if (!Utf8GamePath.FromString(path, out var utf8Path))
return;
var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path);

View file

@ -1,32 +1,41 @@
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;
public class HttpApi : IDisposable
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();
}
@ -42,7 +51,7 @@ public class HttpApi : IDisposable
.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();
@ -57,62 +66,96 @@ public class HttpApi : IDisposable
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.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.RedrawObject(data.ObjectTableIndex, data.Type);
else if (data.Name.Length > 0)
_api.RedrawObject(data.Name, data.Type);
else
_api.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.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.AddMod(data.Path);
api.Mods.AddMod(data.Path);
// Reload the mod by path or name, which will also remove no-longer existing mods.
_api.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.InstallMod(data.Path);
api.Mods.InstallMod(data.Path);
}
public partial void OpenWindow()
{
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
_api.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
{ }
}
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
: 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

@ -0,0 +1,161 @@
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;
public sealed class IpcProviders : IDisposable, IApiService
{
private readonly List<IDisposable> _providers;
private readonly EventProvider _disposedProvider;
private readonly EventProvider _initializedProvider;
private readonly CharacterUtility _characterUtility;
public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api, CharacterUtility characterUtility)
{
_characterUtility = characterUtility;
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
_providers =
[
IpcSubscribers.GetCollections.Provider(pi, api.Collection),
IpcSubscribers.GetCollectionsByIdentifier.Provider(pi, api.Collection),
IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection),
IpcSubscribers.GetCollection.Provider(pi, api.Collection),
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),
IpcSubscribers.GetDrawObjectInfo.Provider(pi, api.GameState),
IpcSubscribers.GetCutsceneParentIndex.Provider(pi, api.GameState),
IpcSubscribers.SetCutsceneParentIndex.Provider(pi, api.GameState),
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),
IpcSubscribers.GetModList.Provider(pi, api.Mods),
IpcSubscribers.InstallMod.Provider(pi, api.Mods),
IpcSubscribers.ReloadMod.Provider(pi, api.Mods),
IpcSubscribers.AddMod.Provider(pi, api.Mods),
IpcSubscribers.DeleteMod.Provider(pi, api.Mods),
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.GetSettingsInAllCollections.Provider(pi, api.ModSettings),
IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModSetting.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.ModSettingChanged.Provider(pi, api.ModSettings),
IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings),
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
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),
IpcSubscribers.ResolveGameObjectPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPath.Provider(pi, api.Resolve),
IpcSubscribers.ReverseResolveGameObjectPath.Provider(pi, api.Resolve),
IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePath.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePaths.Provider(pi, api.Resolve),
IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree),
IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree),
IpcSubscribers.GetGameObjectResourcesOfType.Provider(pi, api.ResourceTree),
IpcSubscribers.GetPlayerResourcesOfType.Provider(pi, api.ResourceTree),
IpcSubscribers.GetGameObjectResourceTrees.Provider(pi, api.ResourceTree),
IpcSubscribers.GetPlayerResourceTrees.Provider(pi, api.ResourceTree),
IpcSubscribers.CreateTemporaryCollection.Provider(pi, api.Temporary),
IpcSubscribers.DeleteTemporaryCollection.Provider(pi, api.Temporary),
IpcSubscribers.AssignTemporaryCollection.Provider(pi, api.Temporary),
IpcSubscribers.AddTemporaryModAll.Provider(pi, api.Temporary),
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),
IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui),
IpcSubscribers.PreSettingsDraw.Provider(pi, api.Ui),
IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui),
IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui),
IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
IpcSubscribers.RegisterSettingsSection.Provider(pi, api.Ui),
IpcSubscribers.UnregisterSettingsSection.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();
_initializedProvider.Dispose();
_disposedProvider.Invoke();
_disposedProvider.Dispose();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,189 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Data;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Penumbra.Api.IpcTester;
public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
{
private int _objectIdx;
private string _collectionIdString = string.Empty;
private Guid? _collectionId;
private bool _allowCreation = true;
private bool _allowDeletion = true;
private ApiCollectionType _type = ApiCollectionType.Yourself;
private Dictionary<Guid, string> _collections = [];
private (string, ChangedItemType, uint)[] _changedItems = [];
private PenumbraApiEc _returnCode = PenumbraApiEc.Success;
private (Guid Id, string Name)? _oldCollection;
public void Draw()
{
using var _ = ImRaii.TreeNode("Collections");
if (!_)
return;
ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName());
ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0);
ImGuiUtil.GuidInput("Collection Id##Collections", "Collection Identifier...", string.Empty, ref _collectionId, ref _collectionIdString);
ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation);
ImGui.SameLine();
ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion);
using var table = ImRaii.Table(string.Empty, 4, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro("Last Return Code", _returnCode.ToString());
if (_oldCollection != null)
ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString());
IpcTester.DrawIntro(GetCollectionsByIdentifier.Label, "Collection Identifier");
var collectionList = new GetCollectionsByIdentifier(pi).Invoke(_collectionIdString);
if (collectionList.Count == 0)
{
DrawCollection(null);
}
else
{
DrawCollection(collectionList[0]);
foreach (var pair in collectionList.Skip(1))
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
DrawCollection(pair);
}
}
IpcTester.DrawIntro(GetCollection.Label, "Current Collection");
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current));
IpcTester.DrawIntro(GetCollection.Label, "Default Collection");
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Default));
IpcTester.DrawIntro(GetCollection.Label, "Interface Collection");
DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Interface));
IpcTester.DrawIntro(GetCollection.Label, "Special Collection");
DrawCollection(new GetCollection(pi).Invoke(_type));
IpcTester.DrawIntro(GetCollections.Label, "Collections");
DrawCollectionPopup();
if (ImGui.Button("Get##Collections"))
{
_collections = new GetCollections(pi).Invoke();
ImGui.OpenPopup("Collections");
}
IpcTester.DrawIntro(GetCollectionForObject.Label, "Get Object Collection");
var (valid, individual, effectiveCollection) = new GetCollectionForObject(pi).Invoke(_objectIdx);
DrawCollection(effectiveCollection);
ImGui.SameLine();
ImGui.TextUnformatted($"({(valid ? "Valid" : "Invalid")} Object{(individual ? ", Individual Assignment)" : ")")}");
IpcTester.DrawIntro(SetCollection.Label, "Set Special Collection");
if (ImGui.Button("Set##SpecialCollection"))
(_returnCode, _oldCollection) =
new SetCollection(pi).Invoke(_type, _collectionId.GetValueOrDefault(Guid.Empty), _allowCreation, _allowDeletion);
ImGui.TableNextColumn();
if (ImGui.Button("Remove##SpecialCollection"))
(_returnCode, _oldCollection) = new SetCollection(pi).Invoke(_type, null, _allowCreation, _allowDeletion);
IpcTester.DrawIntro(SetCollectionForObject.Label, "Set Object Collection");
if (ImGui.Button("Set##ObjectCollection"))
(_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, _collectionId.GetValueOrDefault(Guid.Empty),
_allowCreation, _allowDeletion);
ImGui.TableNextColumn();
if (ImGui.Button("Remove##ObjectCollection"))
(_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, null, _allowCreation, _allowDeletion);
IpcTester.DrawIntro(GetChangedItemsForCollection.Label, "Changed Item List");
DrawChangedItemPopup();
if (ImGui.Button("Get##ChangedItems"))
{
var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
_changedItems = items.Select(kvp =>
{
var (type, id) = kvp.Value.ToApiObject();
return (kvp.Key, type, id);
}).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()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Changed Item List");
if (!p)
return;
using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
{
if (table)
ImGuiClip.ClippedDraw(_changedItems, t =>
{
ImGuiUtil.DrawTableColumn(t.Item1);
ImGuiUtil.DrawTableColumn(t.Item2.ToString());
ImGuiUtil.DrawTableColumn(t.Item3.ToString());
}, ImGui.GetTextLineHeightWithSpacing());
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private void DrawCollectionPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Collections");
if (!p)
return;
using (var t = ImRaii.Table("collections", 2, ImGuiTableFlags.SizingFixedFit))
{
if (t)
foreach (var collection in _collections)
{
ImGui.TableNextColumn();
DrawCollection((collection.Key, collection.Value));
}
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private static void DrawCollection((Guid Id, string Name)? collection)
{
if (collection == null)
{
ImGui.TextUnformatted("<Unassigned>");
ImGui.TableNextColumn();
return;
}
ImGui.TextUnformatted(collection.Value.Name);
ImGui.TableNextColumn();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGuiUtil.CopyOnClickSelectable(collection.Value.Id.ToString());
}
}
}

View file

@ -0,0 +1,70 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class EditingIpcTester(IDalamudPluginInterface pi) : IUiService
{
private string _inputPath = string.Empty;
private string _inputPath2 = string.Empty;
private string _outputPath = string.Empty;
private string _outputPath2 = string.Empty;
private TextureType _typeSelector;
private bool _mipMaps = true;
private Task? _task1;
private Task? _task2;
public void Draw()
{
using var _ = ImRaii.TreeNode("Editing");
if (!_)
return;
ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256);
ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256);
ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256);
ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256);
TypeCombo();
ImGui.Checkbox("Add MipMaps", ref _mipMaps);
using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 1");
if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false }))
_task1 = new ConvertTextureFile(pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps);
ImGui.SameLine();
ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString());
if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted)
ImGui.SetTooltip(_task1.Exception?.ToString());
IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 2");
if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false }))
_task2 = new ConvertTextureFile(pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps);
ImGui.SameLine();
ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString());
if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted)
ImGui.SetTooltip(_task2.Exception?.ToString());
}
private void TypeCombo()
{
using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString());
if (!combo)
return;
foreach (var value in Enum.GetValues<TextureType>())
{
if (ImGui.Selectable(value.ToString(), _typeSelector == value))
_typeSelector = value;
}
}
}

View file

@ -0,0 +1,139 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Plugin;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.String;
namespace Penumbra.Api.IpcTester;
public class GameStateIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<nint, Guid, nint, nint, nint> CharacterBaseCreating;
public readonly EventSubscriber<nint, Guid, nint> CharacterBaseCreated;
public readonly EventSubscriber<nint, string, string> GameObjectResourcePathResolved;
private string _lastCreatedGameObjectName = string.Empty;
private nint _lastCreatedDrawObject = nint.Zero;
private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
private string _lastResolvedGamePath = string.Empty;
private string _lastResolvedFullPath = string.Empty;
private string _lastResolvedObject = string.Empty;
private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue;
private string _currentDrawObjectString = string.Empty;
private nint _currentDrawObject = nint.Zero;
private int _currentCutsceneActor;
private int _currentCutsceneParent;
private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success;
public GameStateIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated);
CharacterBaseCreated = IpcSubscribers.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2);
GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath);
CharacterBaseCreating.Disable();
CharacterBaseCreated.Disable();
GameObjectResourcePathResolved.Disable();
}
public void Dispose()
{
CharacterBaseCreating.Dispose();
CharacterBaseCreated.Dispose();
GameObjectResourcePathResolved.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Game State");
if (!_)
return;
if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16,
ImGuiInputTextFlags.CharsHexadecimal))
_currentDrawObject = nint.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out var tmp)
? tmp
: nint.Zero;
ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0);
ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0);
if (_cutsceneError is not PenumbraApiEc.Success)
{
ImGui.SameLine();
ImGui.TextUnformatted("Invalid Argument on last Call");
}
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(GetDrawObjectInfo.Label, "Draw Object Info");
if (_currentDrawObject == nint.Zero)
{
ImGui.TextUnformatted("Invalid");
}
else
{
var (ptr, (collectionId, collectionName)) = new GetDrawObjectInfo(_pi).Invoke(_currentDrawObject);
ImGui.TextUnformatted(ptr == nint.Zero ? $"No Actor Associated, {collectionName}" : $"{ptr:X}, {collectionName}");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TextUnformatted(collectionId.ToString());
}
}
IpcTester.DrawIntro(GetCutsceneParentIndex.Label, "Cutscene Parent");
ImGui.TextUnformatted(new GetCutsceneParentIndex(_pi).Invoke(_currentCutsceneActor).ToString());
IpcTester.DrawIntro(SetCutsceneParentIndex.Label, "Cutscene Parent");
if (ImGui.Button("Set Parent"))
_cutsceneError = new SetCutsceneParentIndex(_pi)
.Invoke(_currentCutsceneActor, _currentCutsceneParent);
IpcTester.DrawIntro(CreatingCharacterBase.Label, "Last Drawobject created");
if (_lastCreatedGameObjectTime < DateTimeOffset.Now)
ImGui.TextUnformatted(_lastCreatedDrawObject != nint.Zero
? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"
: $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}");
IpcTester.DrawIntro(IpcSubscribers.GameObjectResourcePathResolved.Label, "Last GamePath resolved");
if (_lastResolvedGamePathTime < DateTimeOffset.Now)
ImGui.TextUnformatted(
$"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}");
}
private void UpdateLastCreated(nint gameObject, Guid _, nint _2, nint _3, nint _4)
{
_lastCreatedGameObjectName = GetObjectName(gameObject);
_lastCreatedGameObjectTime = DateTimeOffset.Now;
_lastCreatedDrawObject = nint.Zero;
}
private void UpdateLastCreated2(nint gameObject, Guid _, nint drawObject)
{
_lastCreatedGameObjectName = GetObjectName(gameObject);
_lastCreatedGameObjectTime = DateTimeOffset.Now;
_lastCreatedDrawObject = drawObject;
}
private void UpdateGameObjectResourcePath(nint gameObject, string gamePath, string fullPath)
{
_lastResolvedObject = GetObjectName(gameObject);
_lastResolvedGamePath = gamePath;
_lastResolvedFullPath = fullPath;
_lastResolvedGamePathTime = DateTimeOffset.Now;
}
private static unsafe string GetObjectName(nint gameObject)
{
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject;
return obj != null && obj->Name[0] != 0 ? new ByteString(obj->Name).ToString() : "Unknown";
}
}

View file

@ -0,0 +1,133 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using Dalamud.Bindings.ImGui;
using OtterGui.Services;
using Penumbra.Api.Api;
namespace Penumbra.Api.IpcTester;
public class IpcTester(
IpcProviders ipcProviders,
IPenumbraApi api,
PluginStateIpcTester pluginStateIpcTester,
UiIpcTester uiIpcTester,
RedrawingIpcTester redrawingIpcTester,
GameStateIpcTester gameStateIpcTester,
ResolveIpcTester resolveIpcTester,
CollectionsIpcTester collectionsIpcTester,
MetaIpcTester metaIpcTester,
ModsIpcTester modsIpcTester,
ModSettingsIpcTester modSettingsIpcTester,
EditingIpcTester editingIpcTester,
TemporaryIpcTester temporaryIpcTester,
ResourceTreeIpcTester resourceTreeIpcTester,
IFramework framework) : IUiService
{
private readonly IpcProviders _ipcProviders = ipcProviders;
private DateTime _lastUpdate;
private bool _subscribed = false;
public void Draw()
{
try
{
_lastUpdate = framework.LastUpdateUTC.AddSeconds(1);
Subscribe();
ImGui.TextUnformatted($"API Version: {api.ApiVersion.Breaking}.{api.ApiVersion.Feature:D4}");
collectionsIpcTester.Draw();
editingIpcTester.Draw();
gameStateIpcTester.Draw();
metaIpcTester.Draw();
modSettingsIpcTester.Draw();
modsIpcTester.Draw();
pluginStateIpcTester.Draw();
redrawingIpcTester.Draw();
resolveIpcTester.Draw();
resourceTreeIpcTester.Draw();
uiIpcTester.Draw();
temporaryIpcTester.Draw();
temporaryIpcTester.DrawCollections();
temporaryIpcTester.DrawMods();
}
catch (Exception e)
{
Penumbra.Log.Error($"Error during IPC Tests:\n{e}");
}
}
internal static void DrawIntro(string label, string info)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(label);
ImGui.TableNextColumn();
ImGui.TextUnformatted(info);
ImGui.TableNextColumn();
}
private void Subscribe()
{
if (_subscribed)
return;
Penumbra.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester.");
gameStateIpcTester.GameObjectResourcePathResolved.Enable();
gameStateIpcTester.CharacterBaseCreated.Enable();
gameStateIpcTester.CharacterBaseCreating.Enable();
modSettingsIpcTester.SettingChanged.Enable();
modsIpcTester.DeleteSubscriber.Enable();
modsIpcTester.AddSubscriber.Enable();
modsIpcTester.MoveSubscriber.Enable();
pluginStateIpcTester.ModDirectoryChanged.Enable();
pluginStateIpcTester.Initialized.Enable();
pluginStateIpcTester.Disposed.Enable();
pluginStateIpcTester.EnabledChange.Enable();
redrawingIpcTester.Redrawn.Enable();
uiIpcTester.PreSettingsTabBar.Enable();
uiIpcTester.PreSettingsPanel.Enable();
uiIpcTester.PostEnabled.Enable();
uiIpcTester.PostSettingsPanelDraw.Enable();
uiIpcTester.ChangedItemTooltip.Enable();
uiIpcTester.ChangedItemClicked.Enable();
framework.Update += CheckUnsubscribe;
_subscribed = true;
}
private void CheckUnsubscribe(IFramework framework1)
{
if (_lastUpdate > framework.LastUpdateUTC)
return;
Unsubscribe();
framework.Update -= CheckUnsubscribe;
}
private void Unsubscribe()
{
if (!_subscribed)
return;
Penumbra.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester.");
_subscribed = false;
gameStateIpcTester.GameObjectResourcePathResolved.Disable();
gameStateIpcTester.CharacterBaseCreated.Disable();
gameStateIpcTester.CharacterBaseCreating.Disable();
modSettingsIpcTester.SettingChanged.Disable();
modsIpcTester.DeleteSubscriber.Disable();
modsIpcTester.AddSubscriber.Disable();
modsIpcTester.MoveSubscriber.Disable();
pluginStateIpcTester.ModDirectoryChanged.Disable();
pluginStateIpcTester.Initialized.Disable();
pluginStateIpcTester.Disposed.Disable();
pluginStateIpcTester.EnabledChange.Disable();
redrawingIpcTester.Redrawn.Disable();
uiIpcTester.PreSettingsTabBar.Disable();
uiIpcTester.PreSettingsPanel.Disable();
uiIpcTester.PostEnabled.Disable();
uiIpcTester.PostSettingsPanelDraw.Disable();
uiIpcTester.ChangedItemTooltip.Disable();
uiIpcTester.ChangedItemClicked.Disable();
}
}

View file

@ -0,0 +1,52 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
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 string _metaBase64 = string.Empty;
private MetaDictionary _metaDict = new();
private byte _parsedVersion = byte.MaxValue;
public void Draw()
{
using var _ = ImRaii.TreeNode("Meta");
if (!_)
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;
IpcTester.DrawIntro(GetPlayerMetaManipulations.Label, "Player Meta Manipulations");
if (ImGui.Button("Copy to Clipboard##Player"))
{
var base64 = new GetPlayerMetaManipulations(pi).Invoke();
ImGui.SetClipboardText(base64);
}
IpcTester.DrawIntro(GetMetaManipulations.Label, "Game Object Manipulations");
if (ImGui.Button("Copy to Clipboard##GameObject"))
{
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

@ -0,0 +1,224 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.UI;
namespace Penumbra.Api.IpcTester;
public class ModSettingsIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<ModSettingChange, Guid, string, bool> SettingChanged;
private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
private ModSettingChange _lastSettingChangeType;
private Guid _lastSettingChangeCollection = Guid.Empty;
private string _lastSettingChangeMod = string.Empty;
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 _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)
{
_pi = pi;
SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting);
SettingChanged.Disable();
}
public void Dispose()
{
SettingChanged.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Mod Settings");
if (!_)
return;
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);
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);
if (!table)
return;
IpcTester.DrawIntro("Last Error", _lastSettingsError.ToString());
IpcTester.DrawIntro(ModSettingChanged.Label, "Last Mod Setting Changed");
ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0
? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}"
: "None");
IpcTester.DrawIntro(GetAvailableModSettings.Label, "Get Available Settings");
if (ImGui.Button("Get##Available"))
{
_availableSettings = new GetAvailableModSettings(_pi).Invoke(_settingsModDirectory, _settingsModName);
_lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success;
}
IpcTester.DrawIntro(GetCurrentModSettings.Label, "Get Current Settings");
if (ImGui.Button("Get##Current"))
{
var ret = new GetCurrentModSettings(_pi)
.Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance);
_lastSettingsError = ret.Item1;
if (ret.Item1 == PenumbraApiEc.Success)
{
_settingsEnabled = ret.Item2?.Item1 ?? false;
_settingsInherit = ret.Item2?.Item4 ?? true;
_settingsTemporary = false;
_settingsPriority = ret.Item2?.Item2 ?? 0;
_currentSettings = ret.Item2?.Item3;
}
else
{
_currentSettings = null;
}
}
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();
if (ImGui.Button("Set##Inherit"))
_lastSettingsError = new TryInheritMod(_pi)
.Invoke(collection, _settingsModDirectory, _settingsInherit, _settingsModName);
IpcTester.DrawIntro(TrySetMod.Label, "Set Enabled");
ImGui.Checkbox("##enabled", ref _settingsEnabled);
ImGui.SameLine();
if (ImGui.Button("Set##Enabled"))
_lastSettingsError = new TrySetMod(_pi)
.Invoke(collection, _settingsModDirectory, _settingsEnabled, _settingsModName);
IpcTester.DrawIntro(TrySetModPriority.Label, "Set Priority");
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
ImGui.DragInt("##Priority", ref _settingsPriority);
ImGui.SameLine();
if (ImGui.Button("Set##Priority"))
_lastSettingsError = new TrySetModPriority(_pi)
.Invoke(collection, _settingsModDirectory, _settingsPriority, _settingsModName);
IpcTester.DrawIntro(CopyModSettings.Label, "Copy Mod Settings");
if (ImGui.Button("Copy Settings"))
_lastSettingsError = new CopyModSettings(_pi)
.Invoke(_settingsCollection, _settingsModDirectory, _settingsModName);
ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection.");
IpcTester.DrawIntro(TrySetModSetting.Label, "Set Setting(s)");
if (_availableSettings == null)
return;
foreach (var (group, (list, type)) in _availableSettings)
{
using var id = ImRaii.PushId(group);
var preview = list.Length > 0 ? list[0] : string.Empty;
if (_currentSettings != null && _currentSettings.TryGetValue(group, out var current) && current.Count > 0)
{
preview = current[0];
}
else
{
current = [];
if (_currentSettings != null)
_currentSettings[group] = current;
}
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
using (var c = ImRaii.Combo("##group", preview))
{
if (c)
foreach (var s in list)
{
var contained = current.Contains(s);
if (ImGui.Checkbox(s, ref contained))
{
if (contained)
current.Add(s);
else
current.Remove(s);
}
}
}
ImGui.SameLine();
if (ImGui.Button("Set##setting"))
_lastSettingsError = type == GroupType.Single
? new TrySetModSetting(_pi).Invoke(collection, _settingsModDirectory, group, current.Count > 0 ? current[0] : string.Empty,
_settingsModName)
: new TrySetModSettings(_pi).Invoke(collection, _settingsModDirectory, group, current.ToArray(), _settingsModName);
ImGui.SameLine();
ImGui.TextUnformatted(group);
}
}
private void UpdateLastModSetting(ModSettingChange type, Guid collection, string mod, bool inherited)
{
_lastSettingChangeType = type;
_lastSettingChangeCollection = collection;
_lastSettingChangeMod = mod;
_lastSettingChangeInherited = inherited;
_lastSettingChange = DateTimeOffset.Now;
}
}

View file

@ -0,0 +1,184 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class ModsIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private string _modDirectory = string.Empty;
private string _modName = string.Empty;
private string _pathInput = string.Empty;
private string _newInstallPath = string.Empty;
private PenumbraApiEc _lastReloadEc;
private PenumbraApiEc _lastAddEc;
private PenumbraApiEc _lastDeleteEc;
private PenumbraApiEc _lastSetPathEc;
private PenumbraApiEc _lastInstallEc;
private Dictionary<string, string> _mods = [];
private Dictionary<string, object?> _changedItems = [];
public readonly EventSubscriber<string> DeleteSubscriber;
public readonly EventSubscriber<string> AddSubscriber;
public readonly EventSubscriber<string, string> MoveSubscriber;
private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch;
private string _lastDeletedMod = string.Empty;
private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch;
private string _lastAddedMod = string.Empty;
private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch;
private string _lastMovedModFrom = string.Empty;
private string _lastMovedModTo = string.Empty;
public ModsIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
DeleteSubscriber = ModDeleted.Subscriber(pi, s =>
{
_lastDeletedModTime = DateTimeOffset.UtcNow;
_lastDeletedMod = s;
});
AddSubscriber = ModAdded.Subscriber(pi, s =>
{
_lastAddedModTime = DateTimeOffset.UtcNow;
_lastAddedMod = s;
});
MoveSubscriber = ModMoved.Subscriber(pi, (s1, s2) =>
{
_lastMovedModTime = DateTimeOffset.UtcNow;
_lastMovedModFrom = s1;
_lastMovedModTo = s2;
});
DeleteSubscriber.Disable();
AddSubscriber.Disable();
MoveSubscriber.Disable();
}
public void Dispose()
{
DeleteSubscriber.Dispose();
DeleteSubscriber.Disable();
AddSubscriber.Dispose();
AddSubscriber.Disable();
MoveSubscriber.Dispose();
MoveSubscriber.Disable();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Mods");
if (!_)
return;
ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100);
ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100);
ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100);
ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(GetModList.Label, "Mods");
DrawModsPopup();
if (ImGui.Button("Get##Mods"))
{
_mods = new GetModList(_pi).Invoke();
ImGui.OpenPopup("Mods");
}
IpcTester.DrawIntro(ReloadMod.Label, "Reload Mod");
if (ImGui.Button("Reload"))
_lastReloadEc = new ReloadMod(_pi).Invoke(_modDirectory, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_lastReloadEc.ToString());
IpcTester.DrawIntro(InstallMod.Label, "Install Mod");
if (ImGui.Button("Install"))
_lastInstallEc = new InstallMod(_pi).Invoke(_newInstallPath);
ImGui.SameLine();
ImGui.TextUnformatted(_lastInstallEc.ToString());
IpcTester.DrawIntro(AddMod.Label, "Add Mod");
if (ImGui.Button("Add"))
_lastAddEc = new AddMod(_pi).Invoke(_modDirectory);
ImGui.SameLine();
ImGui.TextUnformatted(_lastAddEc.ToString());
IpcTester.DrawIntro(DeleteMod.Label, "Delete Mod");
if (ImGui.Button("Delete"))
_lastDeleteEc = new DeleteMod(_pi).Invoke(_modDirectory, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_lastDeleteEc.ToString());
IpcTester.DrawIntro(GetChangedItems.Label, "Get Changed Items");
DrawChangedItemsPopup();
if (ImUtf8.Button("Get##ChangedItems"u8))
{
_changedItems = new GetChangedItems(_pi).Invoke(_modDirectory, _modName);
ImUtf8.OpenPopup("ChangedItems"u8);
}
IpcTester.DrawIntro(GetModPath.Label, "Current Path");
var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName);
ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]");
IpcTester.DrawIntro(SetModPath.Label, "Set Path");
if (ImGui.Button("Set"))
_lastSetPathEc = new SetModPath(_pi).Invoke(_modDirectory, _pathInput, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_lastSetPathEc.ToString());
IpcTester.DrawIntro(ModDeleted.Label, "Last Mod Deleted");
if (_lastDeletedModTime > DateTimeOffset.UnixEpoch)
ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}");
IpcTester.DrawIntro(ModAdded.Label, "Last Mod Added");
if (_lastAddedModTime > DateTimeOffset.UnixEpoch)
ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}");
IpcTester.DrawIntro(ModMoved.Label, "Last Mod Moved");
if (_lastMovedModTime > DateTimeOffset.UnixEpoch)
ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}");
}
private void DrawModsPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Mods");
if (!p)
return;
foreach (var (modDir, modName) in _mods)
ImGui.TextUnformatted($"{modDir}: {modName}");
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private void DrawChangedItemsPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImUtf8.Popup("ChangedItems"u8);
if (!p)
return;
foreach (var (name, data) in _changedItems)
ImUtf8.Text($"{name}: {data}");
if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
}

View file

@ -0,0 +1,147 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class PluginStateIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
public readonly EventSubscriber Initialized;
public readonly EventSubscriber Disposed;
public readonly EventSubscriber<bool> EnabledChange;
private string _currentConfiguration = string.Empty;
private string _lastModDirectory = string.Empty;
private bool _lastModDirectoryValid;
private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue;
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;
public PluginStateIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged);
Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized);
Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed);
EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled);
ModDirectoryChanged.Disable();
EnabledChange.Disable();
}
public void Dispose()
{
ModDirectoryChanged.Dispose();
Initialized.Dispose();
Disposed.Dispose();
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;
DrawList(IpcSubscribers.Initialized.Label, "Last Initialized", _initializedList);
DrawList(IpcSubscribers.Disposed.Label, "Last Disposed", _disposedList);
IpcTester.DrawIntro(ApiVersion.Label, "Current Version");
var (breaking, features) = new ApiVersion(_pi).Invoke();
ImGui.TextUnformatted($"{breaking}.{features:D4}");
IpcTester.DrawIntro(GetEnabledState.Label, "Current State");
ImGui.TextUnformatted($"{new GetEnabledState(_pi).Invoke()}");
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"))
{
_currentConfiguration = new GetConfiguration(_pi).Invoke();
ImGui.OpenPopup("Config Popup");
}
IpcTester.DrawIntro(GetModDirectory.Label, "Current Mod Directory");
ImGui.TextUnformatted(new GetModDirectory(_pi).Invoke());
IpcTester.DrawIntro(IpcSubscribers.ModDirectoryChanged.Label, "Last Mod Directory Change");
ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue
? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}"
: "None");
void DrawList(string label, string text, List<DateTimeOffset> list)
{
IpcTester.DrawIntro(label, text);
if (list.Count == 0)
{
ImGui.TextUnformatted("Never");
}
else
{
ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture));
if (list.Count > 1 && ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join("\n",
list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture))));
}
}
}
private void DrawConfigPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var popup = ImRaii.Popup("Config Popup");
if (!popup)
return;
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGuiUtil.TextWrapped(_currentConfiguration);
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
private void UpdateModDirectoryChanged(string path, bool valid)
=> (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now);
private void AddInitialized()
=> _initializedList.Add(DateTimeOffset.UtcNow);
private void AddDisposed()
=> _disposedList.Add(DateTimeOffset.UtcNow);
private void SetLastEnabled(bool val)
=> (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val);
}

View file

@ -0,0 +1,73 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.GameData.Interop;
using Penumbra.UI;
namespace Penumbra.Api.IpcTester;
public class RedrawingIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private readonly ObjectManager _objects;
public readonly EventSubscriber<nint, int> Redrawn;
private int _redrawIndex;
private string _lastRedrawnString = "None";
public RedrawingIpcTester(IDalamudPluginInterface pi, ObjectManager objects)
{
_pi = pi;
_objects = objects;
Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn);
Redrawn.Disable();
}
public void Dispose()
{
Redrawn.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("Redrawing");
if (!_)
return;
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(RedrawObject.Label, "Redraw by Index");
var tmp = _redrawIndex;
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount))
_redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount);
ImGui.SameLine();
if (ImGui.Button("Redraw##Index"))
new RedrawObject(_pi).Invoke(_redrawIndex);
IpcTester.DrawIntro(RedrawAll.Label, "Redraw All");
if (ImGui.Button("Redraw##All"))
new RedrawAll(_pi).Invoke();
IpcTester.DrawIntro(GameObjectRedrawn.Label, "Last Redrawn Object:");
ImGui.TextUnformatted(_lastRedrawnString);
}
private void SetLastRedrawn(nint address, int index)
{
if (index < 0
|| index > _objects.TotalCount
|| address == nint.Zero
|| _objects[index].Address != address)
_lastRedrawnString = "Invalid";
_lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})";
}
}

View file

@ -0,0 +1,114 @@
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.IpcSubscribers;
using Penumbra.String.Classes;
namespace Penumbra.Api.IpcTester;
public class ResolveIpcTester(IDalamudPluginInterface pi) : IUiService
{
private string _currentResolvePath = string.Empty;
private string _currentReversePath = string.Empty;
private int _currentReverseIdx;
private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], []));
public void Draw()
{
using var tree = ImRaii.TreeNode("Resolving");
if (!tree)
return;
ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength);
ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath,
Utf8GamePath.MaxGamePathLength);
ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(ResolveDefaultPath.Label, "Default Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolveDefaultPath(pi).Invoke(_currentResolvePath));
IpcTester.DrawIntro(ResolveInterfacePath.Label, "Interface Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolveInterfacePath(pi).Invoke(_currentResolvePath));
IpcTester.DrawIntro(ResolvePlayerPath.Label, "Player Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolvePlayerPath(pi).Invoke(_currentResolvePath));
IpcTester.DrawIntro(ResolveGameObjectPath.Label, "Game Object Collection Resolve");
if (_currentResolvePath.Length != 0)
ImGui.TextUnformatted(new ResolveGameObjectPath(pi).Invoke(_currentResolvePath, _currentReverseIdx));
IpcTester.DrawIntro(ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)");
if (_currentReversePath.Length > 0)
{
var list = new ReverseResolvePlayerPath(pi).Invoke(_currentReversePath);
if (list.Length > 0)
{
ImGui.TextUnformatted(list[0]);
if (list.Length > 1 && ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
}
}
IpcTester.DrawIntro(ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)");
if (_currentReversePath.Length > 0)
{
var list = new ReverseResolveGameObjectPath(pi).Invoke(_currentReversePath, _currentReverseIdx);
if (list.Length > 0)
{
ImGui.TextUnformatted(list[0]);
if (list.Length > 1 && ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
}
}
var forwardArray = _currentResolvePath.Length > 0
? [_currentResolvePath]
: Array.Empty<string>();
var reverseArray = _currentReversePath.Length > 0
? [_currentReversePath]
: Array.Empty<string>();
IpcTester.DrawIntro(ResolvePlayerPaths.Label, "Resolved Paths (Player)");
if (forwardArray.Length > 0 || reverseArray.Length > 0)
{
var ret = new ResolvePlayerPaths(pi).Invoke(forwardArray, reverseArray);
ImGui.TextUnformatted(ConvertText(ret));
}
IpcTester.DrawIntro(ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)");
if (ImGui.Button("Start"))
_task = new ResolvePlayerPathsAsync(pi).Invoke(forwardArray, reverseArray);
var hovered = ImGui.IsItemHovered();
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(_task.Status.ToString());
if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully)
ImGui.SetTooltip(ConvertText(_task.Result));
return;
static string ConvertText((string[], string[][]) data)
{
var text = string.Empty;
if (data.Item1.Length > 0)
{
if (data.Item2.Length > 0)
text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}.";
else
text = $"Forward: {data.Item1[0]}.";
}
else if (data.Item2.Length > 0)
{
text = $"Reverse: {string.Join("; ", data.Item2[0])}.";
}
return text;
}
}
}

View file

@ -0,0 +1,350 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Penumbra.Api.IpcTester;
public class ResourceTreeIpcTester(IDalamudPluginInterface pi, ObjectManager objects) : IUiService
{
private readonly Stopwatch _stopwatch = new();
private string _gameObjectIndices = "0";
private ResourceType _type = ResourceType.Mtrl;
private bool _withUiData;
private (string, Dictionary<string, HashSet<string>>?)[]? _lastGameObjectResourcePaths;
private (string, Dictionary<string, HashSet<string>>?)[]? _lastPlayerResourcePaths;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastGameObjectResourcesOfType;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastPlayerResourcesOfType;
private (string, ResourceTreeDto?)[]? _lastGameObjectResourceTrees;
private (string, ResourceTreeDto)[]? _lastPlayerResourceTrees;
private TimeSpan _lastCallDuration;
public void Draw()
{
using var _ = ImRaii.TreeNode("Resource Tree");
if (!_)
return;
ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511);
ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues<ResourceType>());
ImGui.Checkbox("Also get names and icons", ref _withUiData);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(GetGameObjectResourcePaths.Label, "Get GameObject resource paths");
if (ImGui.Button("Get##GameObjectResourcePaths"))
{
var gameObjects = GetSelectedGameObjects();
var subscriber = new GetGameObjectResourcePaths(pi);
_stopwatch.Restart();
var resourcePaths = subscriber.Invoke(gameObjects);
_lastCallDuration = _stopwatch.Elapsed;
_lastGameObjectResourcePaths = gameObjects
.Select(i => GameObjectToString(i))
.Zip(resourcePaths)
.ToArray();
ImGui.OpenPopup(nameof(GetGameObjectResourcePaths));
}
IpcTester.DrawIntro(GetPlayerResourcePaths.Label, "Get local player resource paths");
if (ImGui.Button("Get##PlayerResourcePaths"))
{
var subscriber = new GetPlayerResourcePaths(pi);
_stopwatch.Restart();
var resourcePaths = subscriber.Invoke();
_lastCallDuration = _stopwatch.Elapsed;
_lastPlayerResourcePaths = resourcePaths
.Select(pair => (GameObjectToString(pair.Key), pair.Value))
.ToArray()!;
ImGui.OpenPopup(nameof(GetPlayerResourcePaths));
}
IpcTester.DrawIntro(GetGameObjectResourcesOfType.Label, "Get GameObject resources of type");
if (ImGui.Button("Get##GameObjectResourcesOfType"))
{
var gameObjects = GetSelectedGameObjects();
var subscriber = new GetGameObjectResourcesOfType(pi);
_stopwatch.Restart();
var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects);
_lastCallDuration = _stopwatch.Elapsed;
_lastGameObjectResourcesOfType = gameObjects
.Select(i => GameObjectToString(i))
.Zip(resourcesOfType)
.ToArray();
ImGui.OpenPopup(nameof(GetGameObjectResourcesOfType));
}
IpcTester.DrawIntro(GetPlayerResourcesOfType.Label, "Get local player resources of type");
if (ImGui.Button("Get##PlayerResourcesOfType"))
{
var subscriber = new GetPlayerResourcesOfType(pi);
_stopwatch.Restart();
var resourcesOfType = subscriber.Invoke(_type, _withUiData);
_lastCallDuration = _stopwatch.Elapsed;
_lastPlayerResourcesOfType = resourcesOfType
.Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)pair.Value))
.ToArray();
ImGui.OpenPopup(nameof(GetPlayerResourcesOfType));
}
IpcTester.DrawIntro(GetGameObjectResourceTrees.Label, "Get GameObject resource trees");
if (ImGui.Button("Get##GameObjectResourceTrees"))
{
var gameObjects = GetSelectedGameObjects();
var subscriber = new GetGameObjectResourceTrees(pi);
_stopwatch.Restart();
var trees = subscriber.Invoke(_withUiData, gameObjects);
_lastCallDuration = _stopwatch.Elapsed;
_lastGameObjectResourceTrees = gameObjects
.Select(i => GameObjectToString(i))
.Zip(trees)
.ToArray();
ImGui.OpenPopup(nameof(GetGameObjectResourceTrees));
}
IpcTester.DrawIntro(GetPlayerResourceTrees.Label, "Get local player resource trees");
if (ImGui.Button("Get##PlayerResourceTrees"))
{
var subscriber = new GetPlayerResourceTrees(pi);
_stopwatch.Restart();
var trees = subscriber.Invoke(_withUiData);
_lastCallDuration = _stopwatch.Elapsed;
_lastPlayerResourceTrees = trees
.Select(pair => (GameObjectToString(pair.Key), pair.Value))
.ToArray();
ImGui.OpenPopup(nameof(GetPlayerResourceTrees));
}
DrawPopup(nameof(GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths,
_lastCallDuration);
DrawPopup(nameof(GetPlayerResourcePaths), ref _lastPlayerResourcePaths!, DrawResourcePaths, _lastCallDuration);
DrawPopup(nameof(GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType,
_lastCallDuration);
DrawPopup(nameof(GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType,
_lastCallDuration);
DrawPopup(nameof(GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees,
_lastCallDuration);
DrawPopup(nameof(GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration);
}
private static void DrawPopup<T>(string popupId, ref T? result, Action<T> drawResult, TimeSpan duration) where T : class
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500));
using var popup = ImRaii.Popup(popupId);
if (!popup)
{
result = null;
return;
}
if (result == null)
{
ImGui.CloseCurrentPopup();
return;
}
drawResult(result);
ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms");
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
{
result = null;
ImGui.CloseCurrentPopup();
}
}
private static void DrawWithHeaders<T>((string, T?)[] result, Action<T> drawItem) where T : class
{
var firstSeen = new Dictionary<T, string>();
foreach (var (label, item) in result)
{
if (item == null)
{
ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose();
continue;
}
if (firstSeen.TryGetValue(item, out var firstLabel))
{
ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose();
continue;
}
firstSeen.Add(item, label);
using var header = ImRaii.TreeNode(label);
if (!header)
continue;
drawItem(item);
}
}
private static void DrawResourcePaths((string, Dictionary<string, HashSet<string>>?)[] result)
{
DrawWithHeaders(result, paths =>
{
using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f);
ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f);
ImGui.TableHeadersRow();
foreach (var (actualPath, gamePaths) in paths)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(actualPath);
ImGui.TableNextColumn();
foreach (var gamePath in gamePaths)
ImGui.TextUnformatted(gamePath);
}
});
}
private void DrawResourcesOfType((string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[] result)
{
DrawWithHeaders(result, resources =>
{
using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f);
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f);
if (_withUiData)
ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f);
ImGui.TableHeadersRow();
foreach (var (resourceHandle, (actualPath, name, icon)) in resources)
{
ImGui.TableNextColumn();
TextUnformattedMono($"0x{resourceHandle:X}");
ImGui.TableNextColumn();
ImGui.TextUnformatted(actualPath);
if (_withUiData)
{
ImGui.TableNextColumn();
TextUnformattedMono(icon.ToString());
ImGui.SameLine();
ImGui.TextUnformatted(name);
}
}
});
}
private void DrawResourceTrees((string, ResourceTreeDto?)[] result)
{
DrawWithHeaders(result, tree =>
{
ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}");
using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable);
if (!table)
return;
if (_withUiData)
{
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f);
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f);
ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f);
}
else
{
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f);
}
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableHeadersRow();
void DrawNode(ResourceNodeDto node)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
var hasChildren = node.Children.Any();
using var treeNode = ImRaii.TreeNode(
$"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}",
hasChildren
? ImGuiTreeNodeFlags.SpanFullWidth
: ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen);
if (_withUiData)
{
ImGui.TableNextColumn();
TextUnformattedMono(node.Type.ToString());
ImGui.TableNextColumn();
TextUnformattedMono(node.Icon.ToString());
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(node.GamePath ?? "Unknown");
ImGui.TableNextColumn();
ImGui.TextUnformatted(node.ActualPath);
ImGui.TableNextColumn();
TextUnformattedMono($"0x{node.ObjectAddress:X8}");
ImGui.TableNextColumn();
TextUnformattedMono($"0x{node.ResourceHandle:X8}");
if (treeNode)
foreach (var child in node.Children)
DrawNode(child);
}
foreach (var node in tree.Nodes)
DrawNode(node);
});
}
private static void TextUnformattedMono(string text)
{
using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
ImGui.TextUnformatted(text);
}
private ushort[] GetSelectedGameObjects()
=> _gameObjectIndices.Split(',')
.SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i))
.ToArray();
private unsafe string GameObjectToString(ObjectIndex gameObjectIndex)
{
var gameObject = objects[gameObjectIndex];
return gameObject.Valid
? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})"
: $"[{gameObjectIndex}] null";
}
}

View file

@ -0,0 +1,319 @@
using Dalamud.Interface;
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Api.IpcTester;
public class TemporaryIpcTester(
IDalamudPluginInterface pi,
ModManager modManager,
CollectionManager collections,
TempModManager tempMods,
TempCollectionManager tempCollections,
SaveService saveService,
Configuration config)
: IUiService
{
public Guid LastCreatedCollectionId = Guid.Empty;
private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9;
private Guid? _tempGuid;
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;
public void Draw()
{
using var _ = ImRaii.TreeNode("Temporary");
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);
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro("Last Error", _lastTempError.ToString());
ImGuiUtil.DrawTableColumn("Last Created Collection");
ImGui.TableNextColumn();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGuiUtil.CopyOnClickSelectable(LastCreatedCollectionId.ToString());
}
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
if (ImGui.Button("Create##Collection"))
{
_lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
if (_tempGuid == null)
{
_tempGuid = LastCreatedCollectionId;
_tempCollectionGuidName = LastCreatedCollectionId.ToString();
}
}
var guid = _tempGuid.GetValueOrDefault(Guid.Empty);
IpcTester.DrawIntro(DeleteTemporaryCollection.Label, "Delete Temporary Collection");
if (ImGui.Button("Delete##Collection"))
_lastTempError = new DeleteTemporaryCollection(pi).Invoke(guid);
ImGui.SameLine();
if (ImGui.Button("Delete Last##Collection"))
_lastTempError = new DeleteTemporaryCollection(pi).Invoke(LastCreatedCollectionId);
IpcTester.DrawIntro(AssignTemporaryCollection.Label, "Assign Temporary Collection");
if (ImGui.Button("Assign##NamedCollection"))
_lastTempError = new AssignTemporaryCollection(pi).Invoke(guid, _tempActorIndex, _forceOverwrite);
IpcTester.DrawIntro(AddTemporaryMod.Label, "Add Temporary Mod to specific Collection");
if (ImGui.Button("Add##Mod"))
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid,
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Copy Existing Collection");
if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
"Copies the effective list from the collection named in Temporary Mod Name...",
!collections.Storage.ByName(_tempModName, out var copyCollection))
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
var manips = MetaApi.CompressMetaManipulations(copyCollection);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
}
IpcTester.DrawIntro(AddTemporaryModAll.Label, "Add Temporary Mod to all Collections");
if (ImGui.Button("Add##All"))
_lastTempError = new AddTemporaryModAll(pi).Invoke(_tempModName,
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
IpcTester.DrawIntro(RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection");
if (ImGui.Button("Remove##Mod"))
_lastTempError = new RemoveTemporaryMod(pi).Invoke(_tempModName, guid, int.MaxValue);
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()
{
using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8);
if (!collTree)
return;
using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
foreach (var (collection, idx) in tempCollections.Values.WithIndex())
{
using var id = ImRaii.PushId(idx);
ImGui.TableNextColumn();
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
.FirstOrDefault()
?? "Unknown";
if (_debug && ImUtf8.Button("Save##Collection"u8))
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(collection.Identity.Identifier);
}
ImGuiUtil.DrawTableColumn(collection.Identity.Name);
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
ImGuiUtil.DrawTableColumn(string.Join(", ",
tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
}
}
public void DrawMods()
{
using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods");
if (!modTree)
return;
using var table = ImRaii.Table("##modTree", 5, ImGuiTableFlags.SizingFixedFit);
void PrintList(string collectionName, IReadOnlyList<TemporaryMod> list)
{
foreach (var mod in list)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Name.Text);
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Priority.ToString());
ImGui.TableNextColumn();
ImGui.TextUnformatted(collectionName);
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Default.Files.Count.ToString());
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
foreach (var (path, file) in mod.Default.Files)
ImGui.TextUnformatted($"{path} -> {file}");
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.TotalManipulations.ToString());
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
foreach (var identifier in mod.Default.Manipulations.Identifiers)
ImGui.TextUnformatted(identifier.ToString());
}
}
}
if (table)
{
PrintList("All", tempMods.ModsForAllCollections);
foreach (var (collection, list) in tempMods.Mods)
PrintList(collection.Identity.Name, list);
}
}
}

View file

@ -0,0 +1,133 @@
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
public class UiIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber<string, float, float> PreSettingsTabBar;
public readonly EventSubscriber<string> PreSettingsPanel;
public readonly EventSubscriber<string> PostEnabled;
public readonly EventSubscriber<string> PostSettingsPanelDraw;
public readonly EventSubscriber<ChangedItemType, uint> ChangedItemTooltip;
public readonly EventSubscriber<MouseButton, ChangedItemType, uint> ChangedItemClicked;
private string _lastDrawnMod = string.Empty;
private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue;
private bool _subscribedToTooltip;
private bool _subscribedToClick;
private string _lastClicked = string.Empty;
private string _lastHovered = string.Empty;
private TabType _selectTab = TabType.None;
private string _modName = string.Empty;
private PenumbraApiEc _ec = PenumbraApiEc.Success;
public UiIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod);
PreSettingsPanel = IpcSubscribers.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod);
PostSettingsPanelDraw = IpcSubscribers.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip);
ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick);
PreSettingsTabBar.Disable();
PreSettingsPanel.Disable();
PostEnabled.Disable();
PostSettingsPanelDraw.Disable();
ChangedItemTooltip.Disable();
ChangedItemClicked.Disable();
}
public void Dispose()
{
PreSettingsTabBar.Dispose();
PreSettingsPanel.Dispose();
PostEnabled.Dispose();
PostSettingsPanelDraw.Dispose();
ChangedItemTooltip.Dispose();
ChangedItemClicked.Dispose();
}
public void Draw()
{
using var _ = ImRaii.TreeNode("UI");
if (!_)
return;
using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString()))
{
if (combo)
foreach (var val in Enum.GetValues<TabType>())
{
if (ImGui.Selectable(val.ToString(), _selectTab == val))
_selectTab = val;
}
}
ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
IpcTester.DrawIntro(IpcSubscribers.PostSettingsDraw.Label, "Last Drawn Mod");
ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None");
IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip");
if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip))
{
if (_subscribedToTooltip)
ChangedItemTooltip.Enable();
else
ChangedItemTooltip.Disable();
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastHovered);
IpcTester.DrawIntro(IpcSubscribers.ChangedItemClicked.Label, "Subscribe Click");
if (ImGui.Checkbox("##click", ref _subscribedToClick))
{
if (_subscribedToClick)
ChangedItemClicked.Enable();
else
ChangedItemClicked.Disable();
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastClicked);
IpcTester.DrawIntro(OpenMainWindow.Label, "Open Mod Window");
if (ImGui.Button("Open##window"))
_ec = new OpenMainWindow(_pi).Invoke(_selectTab, _modName, _modName);
ImGui.SameLine();
ImGui.TextUnformatted(_ec.ToString());
IpcTester.DrawIntro(CloseMainWindow.Label, "Close Mod Window");
if (ImGui.Button("Close##window"))
new CloseMainWindow(_pi).Invoke();
}
private void UpdateLastDrawnMod(string name)
=> (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
private void UpdateLastDrawnMod(string name, float _1, float _2)
=> (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
private void AddedTooltip(ChangedItemType type, uint id)
{
_lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
ImGui.TextUnformatted("IPC Test Successful");
}
private void AddedClick(MouseButton button, ChangedItemType type, uint id)
{
_lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
}
}

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

File diff suppressed because it is too large Load diff

View file

@ -1,427 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Api;
using CurrentSettings = ValueTuple<PenumbraApiEc, (bool, int, IDictionary<string, IList<string>>, bool)?>;
public class PenumbraIpcProviders : IDisposable
{
internal readonly IPenumbraApi Api;
// Plugin State
internal readonly EventProvider Initialized;
internal readonly EventProvider Disposed;
internal readonly FuncProvider<int> ApiVersion;
internal readonly FuncProvider<(int Breaking, int Features)> ApiVersions;
internal readonly FuncProvider<bool> GetEnabledState;
internal readonly EventProvider<bool> EnabledChange;
// Configuration
internal readonly FuncProvider<string> GetModDirectory;
internal readonly FuncProvider<string> GetConfiguration;
internal readonly EventProvider<string, bool> ModDirectoryChanged;
// UI
internal readonly EventProvider<string> PreSettingsDraw;
internal readonly EventProvider<string> PostSettingsDraw;
internal readonly EventProvider<ChangedItemType, uint> ChangedItemTooltip;
internal readonly EventProvider<MouseButton, ChangedItemType, uint> ChangedItemClick;
internal readonly FuncProvider<TabType, string, string, PenumbraApiEc> OpenMainWindow;
internal readonly ActionProvider CloseMainWindow;
// Redrawing
internal readonly ActionProvider<RedrawType> RedrawAll;
internal readonly ActionProvider<GameObject, RedrawType> RedrawObject;
internal readonly ActionProvider<int, RedrawType> RedrawObjectByIndex;
internal readonly ActionProvider<string, RedrawType> RedrawObjectByName;
internal readonly EventProvider<nint, int> GameObjectRedrawn;
// Game State
internal readonly FuncProvider<nint, (nint, string)> GetDrawObjectInfo;
internal readonly FuncProvider<int, int> GetCutsceneParentIndex;
internal readonly FuncProvider<int, int, PenumbraApiEc> SetCutsceneParentIndex;
internal readonly EventProvider<nint, string, nint, nint, nint> CreatingCharacterBase;
internal readonly EventProvider<nint, string, nint> CreatedCharacterBase;
internal readonly EventProvider<nint, string, string> GameObjectResourcePathResolved;
// Resolve
internal readonly FuncProvider<string, string> ResolveDefaultPath;
internal readonly FuncProvider<string, string> ResolveInterfacePath;
internal readonly FuncProvider<string, string> ResolvePlayerPath;
internal readonly FuncProvider<string, int, string> ResolveGameObjectPath;
internal readonly FuncProvider<string, string, string> ResolveCharacterPath;
internal readonly FuncProvider<string, string, string[]> ReverseResolvePath;
internal readonly FuncProvider<string, int, string[]> ReverseResolveGameObjectPath;
internal readonly FuncProvider<string, string[]> ReverseResolvePlayerPath;
internal readonly FuncProvider<string[], string[], (string[], string[][])> ResolvePlayerPaths;
internal readonly FuncProvider<string[], string[], Task<(string[], string[][])>> ResolvePlayerPathsAsync;
// Collections
internal readonly FuncProvider<IList<string>> GetCollections;
internal readonly FuncProvider<string> GetCurrentCollectionName;
internal readonly FuncProvider<string> GetDefaultCollectionName;
internal readonly FuncProvider<string> GetInterfaceCollectionName;
internal readonly FuncProvider<string, (string, bool)> GetCharacterCollectionName;
internal readonly FuncProvider<ApiCollectionType, string> GetCollectionForType;
internal readonly FuncProvider<ApiCollectionType, string, bool, bool, (PenumbraApiEc, string)> SetCollectionForType;
internal readonly FuncProvider<int, (bool, bool, string)> GetCollectionForObject;
internal readonly FuncProvider<int, string, bool, bool, (PenumbraApiEc, string)> SetCollectionForObject;
internal readonly FuncProvider<string, IReadOnlyDictionary<string, object?>> GetChangedItems;
// Meta
internal readonly FuncProvider<string> GetPlayerMetaManipulations;
internal readonly FuncProvider<string, string> GetMetaManipulations;
internal readonly FuncProvider<int, string> GetGameObjectMetaManipulations;
// Mods
internal readonly FuncProvider<IList<(string, string)>> GetMods;
internal readonly FuncProvider<string, string, PenumbraApiEc> ReloadMod;
internal readonly FuncProvider<string, PenumbraApiEc> InstallMod;
internal readonly FuncProvider<string, PenumbraApiEc> AddMod;
internal readonly FuncProvider<string, string, PenumbraApiEc> DeleteMod;
internal readonly FuncProvider<string, string, (PenumbraApiEc, string, bool)> GetModPath;
internal readonly FuncProvider<string, string, string, PenumbraApiEc> SetModPath;
internal readonly EventProvider<string> ModDeleted;
internal readonly EventProvider<string> ModAdded;
internal readonly EventProvider<string, string> ModMoved;
// ModSettings
internal readonly FuncProvider<string, string, IDictionary<string, (IList<string>, GroupType)>?> GetAvailableModSettings;
internal readonly FuncProvider<string, string, string, bool, CurrentSettings> GetCurrentModSettings;
internal readonly FuncProvider<string, string, string, bool, PenumbraApiEc> TryInheritMod;
internal readonly FuncProvider<string, string, string, bool, PenumbraApiEc> TrySetMod;
internal readonly FuncProvider<string, string, string, int, PenumbraApiEc> TrySetModPriority;
internal readonly FuncProvider<string, string, string, string, string, PenumbraApiEc> TrySetModSetting;
internal readonly FuncProvider<string, string, string, string, IReadOnlyList<string>, PenumbraApiEc> TrySetModSettings;
internal readonly EventProvider<ModSettingChange, string, string, bool> ModSettingChanged;
internal readonly FuncProvider<string, string, string, PenumbraApiEc> CopyModSettings;
// Editing
internal readonly FuncProvider<string, string, TextureType, bool, Task> ConvertTextureFile;
internal readonly FuncProvider<byte[], int, string, TextureType, bool, Task> ConvertTextureData;
// Temporary
internal readonly FuncProvider<string, string, bool, (PenumbraApiEc, string)> CreateTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> RemoveTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> CreateNamedTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> RemoveTemporaryCollectionByName;
internal readonly FuncProvider<string, int, bool, PenumbraApiEc> AssignTemporaryCollection;
internal readonly FuncProvider<string, Dictionary<string, string>, string, int, PenumbraApiEc> AddTemporaryModAll;
internal readonly FuncProvider<string, string, Dictionary<string, string>, string, int, PenumbraApiEc> AddTemporaryMod;
internal readonly FuncProvider<string, int, PenumbraApiEc> RemoveTemporaryModAll;
internal readonly FuncProvider<string, string, int, PenumbraApiEc> RemoveTemporaryMod;
// Resource Tree
internal readonly FuncProvider<ushort[], IReadOnlyDictionary<string, string[]>?[]> GetGameObjectResourcePaths;
internal readonly FuncProvider<IReadOnlyDictionary<ushort, IReadOnlyDictionary<string, string[]>>> GetPlayerResourcePaths;
internal readonly FuncProvider<ResourceType, bool, ushort[], IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?[]>
GetGameObjectResourcesOfType;
internal readonly
FuncProvider<ResourceType, bool, IReadOnlyDictionary<ushort, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>>>
GetPlayerResourcesOfType;
internal readonly FuncProvider<bool, ushort[], Ipc.ResourceTree?[]> GetGameObjectResourceTrees;
internal readonly FuncProvider<bool, IReadOnlyDictionary<ushort, Ipc.ResourceTree>> GetPlayerResourceTrees;
public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections,
TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config)
{
Api = api;
// Plugin State
Initialized = Ipc.Initialized.Provider(pi);
Disposed = Ipc.Disposed.Provider(pi);
ApiVersion = Ipc.ApiVersion.Provider(pi, DeprecatedVersion);
ApiVersions = Ipc.ApiVersions.Provider(pi, () => Api.ApiVersion);
GetEnabledState = Ipc.GetEnabledState.Provider(pi, Api.GetEnabledState);
EnabledChange =
Ipc.EnabledChange.Provider(pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent);
// Configuration
GetModDirectory = Ipc.GetModDirectory.Provider(pi, Api.GetModDirectory);
GetConfiguration = Ipc.GetConfiguration.Provider(pi, Api.GetConfiguration);
ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a);
// UI
PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a);
PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a);
ChangedItemTooltip =
Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip);
ChangedItemClick = Ipc.ChangedItemClick.Provider(pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick);
OpenMainWindow = Ipc.OpenMainWindow.Provider(pi, Api.OpenMainWindow);
CloseMainWindow = Ipc.CloseMainWindow.Provider(pi, Api.CloseMainWindow);
// Redrawing
RedrawAll = Ipc.RedrawAll.Provider(pi, Api.RedrawAll);
RedrawObject = Ipc.RedrawObject.Provider(pi, Api.RedrawObject);
RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider(pi, Api.RedrawObject);
RedrawObjectByName = Ipc.RedrawObjectByName.Provider(pi, Api.RedrawObject);
GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider(pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn,
() => Api.GameObjectRedrawn -= OnGameObjectRedrawn);
// Game State
GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider(pi, Api.GetDrawObjectInfo);
GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider(pi, Api.GetCutsceneParentIndex);
SetCutsceneParentIndex = Ipc.SetCutsceneParentIndex.Provider(pi, Api.SetCutsceneParentIndex);
CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider(pi,
() => Api.CreatingCharacterBase += CreatingCharacterBaseEvent,
() => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent);
CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider(pi,
() => Api.CreatedCharacterBase += CreatedCharacterBaseEvent,
() => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent);
GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider(pi,
() => Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent,
() => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent);
// Resolve
ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider(pi, Api.ResolveDefaultPath);
ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider(pi, Api.ResolveInterfacePath);
ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider(pi, Api.ResolvePlayerPath);
ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider(pi, Api.ResolveGameObjectPath);
ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider(pi, Api.ResolvePath);
ReverseResolvePath = Ipc.ReverseResolvePath.Provider(pi, Api.ReverseResolvePath);
ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider(pi, Api.ReverseResolveGameObjectPath);
ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider(pi, Api.ReverseResolvePlayerPath);
ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider(pi, Api.ResolvePlayerPaths);
ResolvePlayerPathsAsync = Ipc.ResolvePlayerPathsAsync.Provider(pi, Api.ResolvePlayerPathsAsync);
// Collections
GetCollections = Ipc.GetCollections.Provider(pi, Api.GetCollections);
GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider(pi, Api.GetCurrentCollection);
GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider(pi, Api.GetDefaultCollection);
GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider(pi, Api.GetInterfaceCollection);
GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider(pi, Api.GetCharacterCollection);
GetCollectionForType = Ipc.GetCollectionForType.Provider(pi, Api.GetCollectionForType);
SetCollectionForType = Ipc.SetCollectionForType.Provider(pi, Api.SetCollectionForType);
GetCollectionForObject = Ipc.GetCollectionForObject.Provider(pi, Api.GetCollectionForObject);
SetCollectionForObject = Ipc.SetCollectionForObject.Provider(pi, Api.SetCollectionForObject);
GetChangedItems = Ipc.GetChangedItems.Provider(pi, Api.GetChangedItemsForCollection);
// Meta
GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider(pi, Api.GetPlayerMetaManipulations);
GetMetaManipulations = Ipc.GetMetaManipulations.Provider(pi, Api.GetMetaManipulations);
GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider(pi, Api.GetGameObjectMetaManipulations);
// Mods
GetMods = Ipc.GetMods.Provider(pi, Api.GetModList);
ReloadMod = Ipc.ReloadMod.Provider(pi, Api.ReloadMod);
InstallMod = Ipc.InstallMod.Provider(pi, Api.InstallMod);
AddMod = Ipc.AddMod.Provider(pi, Api.AddMod);
DeleteMod = Ipc.DeleteMod.Provider(pi, Api.DeleteMod);
GetModPath = Ipc.GetModPath.Provider(pi, Api.GetModPath);
SetModPath = Ipc.SetModPath.Provider(pi, Api.SetModPath);
ModDeleted = Ipc.ModDeleted.Provider(pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent);
ModAdded = Ipc.ModAdded.Provider(pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent);
ModMoved = Ipc.ModMoved.Provider(pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent);
// ModSettings
GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider(pi, Api.GetAvailableModSettings);
GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider(pi, Api.GetCurrentModSettings);
TryInheritMod = Ipc.TryInheritMod.Provider(pi, Api.TryInheritMod);
TrySetMod = Ipc.TrySetMod.Provider(pi, Api.TrySetMod);
TrySetModPriority = Ipc.TrySetModPriority.Provider(pi, Api.TrySetModPriority);
TrySetModSetting = Ipc.TrySetModSetting.Provider(pi, Api.TrySetModSetting);
TrySetModSettings = Ipc.TrySetModSettings.Provider(pi, Api.TrySetModSettings);
ModSettingChanged = Ipc.ModSettingChanged.Provider(pi,
() => Api.ModSettingChanged += ModSettingChangedEvent,
() => Api.ModSettingChanged -= ModSettingChangedEvent);
CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings);
// Editing
ConvertTextureFile = Ipc.ConvertTextureFile.Provider(pi, Api.ConvertTextureFile);
ConvertTextureData = Ipc.ConvertTextureData.Provider(pi, Api.ConvertTextureData);
// Temporary
CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection);
RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection);
CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider(pi, Api.CreateNamedTemporaryCollection);
RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider(pi, Api.RemoveTemporaryCollectionByName);
AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider(pi, Api.AssignTemporaryCollection);
AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider(pi, Api.AddTemporaryModAll);
AddTemporaryMod = Ipc.AddTemporaryMod.Provider(pi, Api.AddTemporaryMod);
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll);
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod);
// ResourceTree
GetGameObjectResourcePaths = Ipc.GetGameObjectResourcePaths.Provider(pi, Api.GetGameObjectResourcePaths);
GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths);
GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType);
GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType);
GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees);
GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees);
Initialized.Invoke();
}
public void Dispose()
{
// Plugin State
Initialized.Dispose();
ApiVersion.Dispose();
ApiVersions.Dispose();
GetEnabledState.Dispose();
EnabledChange.Dispose();
// Configuration
GetModDirectory.Dispose();
GetConfiguration.Dispose();
ModDirectoryChanged.Dispose();
// UI
PreSettingsDraw.Dispose();
PostSettingsDraw.Dispose();
ChangedItemTooltip.Dispose();
ChangedItemClick.Dispose();
OpenMainWindow.Dispose();
CloseMainWindow.Dispose();
// Redrawing
RedrawAll.Dispose();
RedrawObject.Dispose();
RedrawObjectByIndex.Dispose();
RedrawObjectByName.Dispose();
GameObjectRedrawn.Dispose();
// Game State
GetDrawObjectInfo.Dispose();
GetCutsceneParentIndex.Dispose();
SetCutsceneParentIndex.Dispose();
CreatingCharacterBase.Dispose();
CreatedCharacterBase.Dispose();
GameObjectResourcePathResolved.Dispose();
// Resolve
ResolveDefaultPath.Dispose();
ResolveInterfacePath.Dispose();
ResolvePlayerPath.Dispose();
ResolveGameObjectPath.Dispose();
ResolveCharacterPath.Dispose();
ReverseResolvePath.Dispose();
ReverseResolveGameObjectPath.Dispose();
ReverseResolvePlayerPath.Dispose();
ResolvePlayerPaths.Dispose();
ResolvePlayerPathsAsync.Dispose();
// Collections
GetCollections.Dispose();
GetCurrentCollectionName.Dispose();
GetDefaultCollectionName.Dispose();
GetInterfaceCollectionName.Dispose();
GetCharacterCollectionName.Dispose();
GetCollectionForType.Dispose();
SetCollectionForType.Dispose();
GetCollectionForObject.Dispose();
SetCollectionForObject.Dispose();
GetChangedItems.Dispose();
// Meta
GetPlayerMetaManipulations.Dispose();
GetMetaManipulations.Dispose();
GetGameObjectMetaManipulations.Dispose();
// Mods
GetMods.Dispose();
ReloadMod.Dispose();
InstallMod.Dispose();
AddMod.Dispose();
DeleteMod.Dispose();
GetModPath.Dispose();
SetModPath.Dispose();
ModDeleted.Dispose();
ModAdded.Dispose();
ModMoved.Dispose();
// ModSettings
GetAvailableModSettings.Dispose();
GetCurrentModSettings.Dispose();
TryInheritMod.Dispose();
TrySetMod.Dispose();
TrySetModPriority.Dispose();
TrySetModSetting.Dispose();
TrySetModSettings.Dispose();
ModSettingChanged.Dispose();
CopyModSettings.Dispose();
// Temporary
CreateTemporaryCollection.Dispose();
RemoveTemporaryCollection.Dispose();
CreateNamedTemporaryCollection.Dispose();
RemoveTemporaryCollectionByName.Dispose();
AssignTemporaryCollection.Dispose();
AddTemporaryModAll.Dispose();
AddTemporaryMod.Dispose();
RemoveTemporaryModAll.Dispose();
RemoveTemporaryMod.Dispose();
// Editing
ConvertTextureFile.Dispose();
ConvertTextureData.Dispose();
// Resource Tree
GetGameObjectResourcePaths.Dispose();
GetPlayerResourcePaths.Dispose();
GetGameObjectResourcesOfType.Dispose();
GetPlayerResourcesOfType.Dispose();
GetGameObjectResourceTrees.Dispose();
GetPlayerResourceTrees.Dispose();
Disposed.Invoke();
Disposed.Dispose();
}
// Wrappers
private int DeprecatedVersion()
{
Penumbra.Log.Warning($"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead.");
return Api.ApiVersion.Breaking;
}
private void OnClick(MouseButton click, object? item)
{
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item);
ChangedItemClick.Invoke(click, type, id);
}
private void OnTooltip(object? item)
{
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item);
ChangedItemTooltip.Invoke(type, id);
}
private void EnabledChangeEvent(bool value)
=> EnabledChange.Invoke(value);
private void OnGameObjectRedrawn(IntPtr objectAddress, int objectTableIndex)
=> GameObjectRedrawn.Invoke(objectAddress, objectTableIndex);
private void CreatingCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData)
=> CreatingCharacterBase.Invoke(gameObject, collectionName, modelId, customize, equipData);
private void CreatedCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr drawObject)
=> CreatedCharacterBase.Invoke(gameObject, collectionName, drawObject);
private void GameObjectResourceResolvedEvent(IntPtr gameObject, string gamePath, string localPath)
=> GameObjectResourcePathResolved.Invoke(gameObject, gamePath, localPath);
private void ModSettingChangedEvent(ModSettingChange type, string collection, string mod, bool inherited)
=> ModSettingChanged.Invoke(type, collection, mod, inherited);
private void ModDeletedEvent(string name)
=> ModDeleted.Invoke(name);
private void ModAddedEvent(string name)
=> ModAdded.Invoke(name);
private void ModMovedEvent(string from, string to)
=> ModMoved.Invoke(from, to);
}

View file

@ -1,3 +1,4 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
@ -6,6 +7,7 @@ using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Mods.Settings;
namespace Penumbra.Api;
@ -17,12 +19,12 @@ public enum RedirectResult
FilteredGamePath = 3,
}
public class TempModManager : IDisposable
public class TempModManager : IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly Dictionary<ModCollection, List<TemporaryMod>> _mods = new();
private readonly List<TemporaryMod> _modsForAllCollections = new();
private readonly Dictionary<ModCollection, List<TemporaryMod>> _mods = [];
private readonly List<TemporaryMod> _modsForAllCollections = [];
public TempModManager(CommunicatorService communicator)
{
@ -42,7 +44,7 @@ public class TempModManager : IDisposable
=> _modsForAllCollections;
public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
HashSet<MetaManipulation> manips, int priority)
MetaDictionary manips, ModPriority priority)
{
var mod = GetOrCreateMod(tag, collection, priority, out var created);
Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}.");
@ -51,10 +53,10 @@ public class TempModManager : IDisposable
return RedirectResult.Success;
}
public RedirectResult Unregister(string tag, ModCollection? collection, int? priority)
public RedirectResult Unregister(string tag, ModCollection? collection, ModPriority? priority)
{
Penumbra.Log.Verbose($"Removing temporary mod with tag {tag}...");
var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null;
var list = collection == null ? _modsForAllCollections : _mods.GetValueOrDefault(collection);
if (list == null)
return RedirectResult.NotRegistered;
@ -83,15 +85,15 @@ public class TempModManager : IDisposable
{
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, 0, 0, false);
_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, 1, 0, false);
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false);
}
}
else
@ -116,7 +118,7 @@ public class TempModManager : IDisposable
// Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections).
// Returns the found or created mod and whether it was newly created.
private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created)
private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, ModPriority priority, out bool created)
{
List<TemporaryMod> list;
if (collection == null)
@ -129,14 +131,14 @@ public class TempModManager : IDisposable
}
else
{
list = new List<TemporaryMod>();
list = [];
_mods.Add(collection, list);
}
var mod = list.Find(m => m.Priority == priority && m.Name == tag);
if (mod == null)
{
mod = new TemporaryMod()
mod = new TemporaryMod
{
Name = tag,
Priority = priority,

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,56 +0,0 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct CmpCache : IDisposable
{
private CmpFile? _cmpFile = null;
private readonly List<RspManipulation> _cmpManipulations = new();
public CmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public void Reset()
{
if (_cmpFile == null)
return;
_cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute)));
_cmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, RspManipulation manip)
{
_cmpManipulations.AddOrReplace(manip);
_cmpFile ??= new CmpFile(manager);
return manip.Apply(_cmpFile);
}
public bool RevertMod(MetaFileManager manager, RspManipulation manip)
{
if (!_cmpManipulations.Remove(manip))
return false;
var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute);
manip = new RspManipulation(manip.SubRace, manip.Attribute, def);
return manip.Apply(_cmpFile!);
}
public void Dispose()
{
_cmpFile?.Dispose();
_cmpFile = null;
_cmpManipulations.Clear();
}
}

View file

@ -1,13 +1,13 @@
using OtterGui;
using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Util;
using Penumbra.GameData.Data;
using OtterGui.Extensions;
namespace Penumbra.Collections.Cache;
@ -18,31 +18,32 @@ public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority,
/// The Cache contains all required temporary data to use a collection.
/// It will only be setup if a collection gets activated in any way.
/// </summary>
public class CollectionCache : IDisposable
public sealed class CollectionCache : IDisposable
{
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
public int Calculating = -1;
public string AnonymizedName
=> _collection.AnonymizedName;
=> _collection.Identity.AnonymizedName;
public IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> ConflictDict.Values;
public SingleArray<ModConflicts> Conflicts(IMod mod)
=> ConflictDict.TryGetValue(mod, out SingleArray<ModConflicts> c) ? c : new SingleArray<ModConflicts>();
=> ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray<ModConflicts>();
private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
{
get
{
@ -54,16 +55,21 @@ public class CollectionCache : IDisposable
// The cache reacts through events on its collection changing.
public CollectionCache(CollectionCacheManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection);
_manager = manager;
_collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection);
CustomResources = new CustomResourceCache(manager.ResourceLoader);
}
public void Dispose()
=> Meta.Dispose();
{
Meta.Dispose();
CustomResources.Dispose();
GC.SuppressFinalize(this);
}
~CollectionCache()
=> Meta.Dispose();
=> Dispose();
// Resolve a given game path according to this collection.
public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
@ -72,7 +78,7 @@ public class CollectionCache : IDisposable
return null;
if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.Path.IsRooted && !candidate.Path.Exists)
|| candidate.Path is { IsRooted: true, Exists: false })
return null;
return candidate.Path;
@ -100,7 +106,7 @@ public class CollectionCache : IDisposable
public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> fullPaths)
{
if (fullPaths.Count == 0)
return Array.Empty<HashSet<Utf8GamePath>>();
return [];
var ret = new HashSet<Utf8GamePath>[fullPaths.Count];
var dict = new Dictionary<FullPath, int>(fullPaths.Count);
@ -108,8 +114,8 @@ public class CollectionCache : IDisposable
{
dict[new FullPath(path)] = idx;
ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8)
? new HashSet<Utf8GamePath> { utf8 }
: new HashSet<Utf8GamePath>();
? [utf8]
: [];
}
foreach (var (game, full) in ResolvedFiles)
@ -121,12 +127,6 @@ public class CollectionCache : IDisposable
return ret;
}
public void ForceFile(Utf8GamePath path, FullPath fullPath)
=> _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath));
public void RemovePath(Utf8GamePath path)
=> _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty));
public void ReloadMod(IMod mod, bool addMetaChanges)
=> _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges));
@ -148,17 +148,20 @@ public class CollectionCache : IDisposable
if (fullPath.FullName.Length > 0)
{
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path,
Mod.ForcedFiles);
}
else
{
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null);
}
}
else if (fullPath.FullName.Length > 0)
{
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles);
}
}
@ -175,12 +178,13 @@ public class CollectionCache : IDisposable
var (paths, manipulations) = ModData.RemoveMod(mod);
if (addMetaChanges)
_collection.IncrementCounter();
_collection.Counters.IncrementChange();
foreach (var path in paths)
{
if (ResolvedFiles.Remove(path, out var mp))
{
CustomResources.Invalidate(path);
if (mp.Mod != mod)
Penumbra.Log.Warning(
$"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}.");
@ -221,56 +225,50 @@ public class CollectionCache : IDisposable
/// <summary> Add all files and possibly manipulations of a given mod according to its settings in this collection. </summary>
internal void AddModSync(IMod mod, bool addMetaChanges)
{
if (mod.Index >= 0)
var files = GetFiles(mod);
foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod);
if (files.Manipulations.Count > 0)
{
var settings = _collection[mod.Index].Settings;
if (settings is not { Enabled: true })
return;
foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Item1.Priority))
{
if (group.Count == 0)
continue;
var config = settings.Settings[groupIndex];
switch (group.Type)
{
case GroupType.Single:
AddSubMod(group[(int)config], mod);
break;
case GroupType.Multi:
{
foreach (var (option, _) in group.WithIndex()
.Where(p => ((1 << p.Item2) & config) != 0)
.OrderByDescending(p => group.OptionPriority(p.Item2)))
AddSubMod(option, mod);
break;
}
}
}
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!);
}
AddSubMod(mod.Default, mod);
if (addMetaChanges)
{
_collection.IncrementCounter();
if (mod.TotalManipulations > 0)
AddMetaFiles(false);
_collection.Counters.IncrementChange();
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
// Add all files and possibly manipulations of a specific submod
private void AddSubMod(ISubMod subMod, IMod parentMod)
private AppliedModData GetFiles(IMod mod)
{
foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps))
AddFile(path, file, parentMod);
if (mod.Index < 0)
return mod.GetData();
foreach (var manip in subMod.Manipulations)
AddManipulation(manip, parentMod);
var settings = _collection.GetActualSettings(mod.Index).Settings;
return settings is not { Enabled: true }
? AppliedModData.Empty
: mod.GetData(settings);
}
/// <summary> Invoke only if not in a full recalculation. </summary>
@ -281,6 +279,24 @@ public 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.
@ -290,11 +306,15 @@ public class CollectionCache : IDisposable
if (!CheckFullPath(path, file))
return;
if (!IsRedirectionSupported(path, mod))
return;
try
{
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
{
ModData.AddPath(mod, path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod);
return;
}
@ -309,13 +329,14 @@ public class CollectionCache : IDisposable
ModData.RemovePath(modPath.Mod, path);
ResolvedFiles[path] = new ModPath(mod, file);
ModData.AddPath(mod, path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod);
}
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}");
$"[{Environment.CurrentManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}");
}
}
@ -347,8 +368,9 @@ public 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)
{
@ -356,7 +378,7 @@ public class CollectionCache : IDisposable
foreach (var conflict in tmpConflicts)
{
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0)
|| data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0)
AddConflict(data, addedMod, conflict.Mod2);
}
@ -388,12 +410,12 @@ public class CollectionCache : IDisposable
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddManipulation(MetaManipulation manip, IMod mod)
private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry)
{
if (!Meta.TryGetValue(manip, out var existingMod))
if (!Meta.TryGetMod(identifier, out var existingMod))
{
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
return;
}
@ -401,34 +423,29 @@ public class CollectionCache : IDisposable
if (mod == existingMod)
return;
if (AddConflict(manip, mod, existingMod))
if (AddConflict(identifier, mod, existingMod))
{
ModData.RemoveManip(existingMod, manip);
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
ModData.RemoveManip(existingMod, identifier);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
}
}
// Add all necessary meta file redirects.
public void AddMetaFiles(bool fromFullCompute)
=> Meta.SetImcFiles(fromFullCompute);
// 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, object?>(512);
var items = new SortedList<string, IIdentifiedObjectData>(512);
void AddItems(IMod mod)
{
@ -437,8 +454,9 @@ public 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 int x && data.Item2 is int y ? x + y : obj);
else if (obj is int x && data.Item2 is int y)
_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);
}
@ -451,11 +469,14 @@ public class CollectionCache : IDisposable
AddItems(modPath.Mod);
}
foreach (var (manip, mod) in Meta)
foreach (var (manip, mod) in Meta.IdentifierSources)
{
ModCacheManager.ComputeChangedItems(identifier, items, manip);
manip.AddChangedItems(identifier, items);
AddItems(mod);
}
if (_manager.Config.HideMachinistOffhandFromChangedItems)
_changedItems.RemoveMachinistOffhands();
}
catch (Exception e)
{

View file

@ -1,18 +1,24 @@
using Dalamud.Plugin.Services;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class CollectionCacheManager : IDisposable
public class CollectionCacheManager : IDisposable, IService
{
private readonly FrameworkManager _framework;
private readonly CommunicatorService _communicator;
@ -20,9 +26,10 @@ public class CollectionCacheManager : IDisposable
private readonly ModStorage _modStorage;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
internal readonly Configuration Config;
internal readonly ResolvedFileChanged ResolvedFileChanged;
internal readonly MetaFileManager MetaFileManager;
internal readonly MetaFileManager MetaFileManager;
internal readonly ResourceLoader ResourceLoader;
private readonly ConcurrentQueue<CollectionCache.ChangeData> _changeQueue = new();
@ -35,7 +42,8 @@ public class CollectionCacheManager : IDisposable
=> _storage.Where(c => c.HasCache);
public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage,
MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage)
MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader,
Configuration config)
{
_framework = framework;
_communicator = communicator;
@ -44,6 +52,8 @@ public class CollectionCacheManager : IDisposable
MetaFileManager = metaFileManager;
_active = active;
_storage = storage;
ResourceLoader = resourceLoader;
Config = config;
ResolvedFileChanged = _communicator.ResolvedFileChanged;
if (!_active.Individuals.IsLoaded)
@ -61,7 +71,7 @@ public class CollectionCacheManager : IDisposable
_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()
@ -73,7 +83,13 @@ public class CollectionCacheManager : IDisposable
_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)
{
collection._cache?.Dispose();
collection._cache = null;
}
}
public void AddChange(CollectionCache.ChangeData data)
@ -98,16 +114,16 @@ public class CollectionCacheManager : IDisposable
/// <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;
}
@ -116,32 +132,32 @@ public class CollectionCacheManager : IDisposable
/// Does not create caches.
/// </summary>
public void CalculateEffectiveFileList(ModCollection collection)
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name,
=> _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.");
}
}
@ -155,8 +171,7 @@ public class CollectionCacheManager : IDisposable
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();
@ -171,9 +186,7 @@ public class CollectionCacheManager : IDisposable
foreach (var mod in _modStorage)
cache.AddModSync(mod, false);
cache.AddMetaFiles(true);
collection.IncrementCounter();
collection.Counters.IncrementChange();
MetaFileManager.ApplyDefaultFiles(collection);
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty,
@ -199,7 +212,7 @@ public class CollectionCacheManager : IDisposable
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)
@ -217,11 +230,11 @@ public class CollectionCacheManager : IDisposable
{
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;
}
@ -232,7 +245,7 @@ public class CollectionCacheManager : IDisposable
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);
}
@ -244,37 +257,38 @@ public class CollectionCacheManager : IDisposable
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);
}
/// <summary> Prepare Changes by removing mods from caches with collections or add or reload mods. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int movedToIdx)
{
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;
}
type.HandlingInfo(out _, out var recomputeList, out var reload);
type.HandlingInfo(out _, out var recomputeList, out var justAdd);
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 (reload)
collection._cache!.ReloadMod(mod, true);
else
if (justAdd)
collection._cache!.AddMod(mod, true);
else
collection._cache!.ReloadMod(mod, true);
}
}
@ -282,11 +296,11 @@ public class CollectionCacheManager : IDisposable
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, int oldValue, int groupIdx, bool _)
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _)
{
if (!collection.HasCache)
return;
@ -298,11 +312,11 @@ public class CollectionCacheManager : IDisposable
cache.ReloadMod(mod!, true);
break;
case ModSettingChange.EnableState:
if (oldValue == 0)
if (oldValue == Setting.False)
cache.AddMod(mod!, true);
else if (oldValue == 1)
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);
@ -314,10 +328,13 @@ public class CollectionCacheManager : IDisposable
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);
@ -344,9 +361,9 @@ public class CollectionCacheManager : IDisposable
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

@ -1,23 +1,25 @@
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
/// <summary>
/// Contains associations between a mod and the paths and meta manipulations affected by that mod.
/// </summary>
public class CollectionModData
{
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<MetaManipulation>)> _data = new();
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<IMetaIdentifier>)> _data = new();
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<MetaManipulation>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<MetaManipulation>)kvp.Value.Item2));
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<IMetaIdentifier>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<IMetaIdentifier>)kvp.Value.Item2));
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<MetaManipulation> Manipulations) RemoveMod(IMod mod)
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<IMetaIdentifier> Manipulations) RemoveMod(IMod mod)
{
if (_data.Remove(mod, out var data))
return data;
return (Array.Empty<Utf8GamePath>(), Array.Empty<MetaManipulation>());
return ([], []);
}
public void AddPath(IMod mod, Utf8GamePath path)
@ -28,12 +30,12 @@ public class CollectionModData
}
else
{
data = (new HashSet<Utf8GamePath> { path }, new HashSet<MetaManipulation>());
data = ([path], []);
_data.Add(mod, data);
}
}
public void AddManip(IMod mod, MetaManipulation manipulation)
public void AddManip(IMod mod, IMetaIdentifier manipulation)
{
if (_data.TryGetValue(mod, out var data))
{
@ -41,7 +43,7 @@ public class CollectionModData
}
else
{
data = (new HashSet<Utf8GamePath>(), new HashSet<MetaManipulation> { manipulation });
data = ([], [manipulation]);
_data.Add(mod, data);
}
}
@ -52,7 +54,7 @@ public class CollectionModData
_data.Remove(mod);
}
public void RemoveManip(IMod mod, MetaManipulation manip)
public void RemoveManip(IMod mod, IMetaIdentifier manip)
{
if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
_data.Remove(mod);

View file

@ -0,0 +1,49 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.SafeHandles;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
/// <summary> A cache for resources owned by a collection. </summary>
public sealed class CustomResourceCache(ResourceLoader loader)
: ConcurrentDictionary<Utf8GamePath, SafeResourceHandle>, IDisposable
{
/// <summary> Invalidate an existing resource by clearing it from the cache and disposing it. </summary>
public void Invalidate(Utf8GamePath path)
{
if (TryRemove(path, out var handle))
handle.Dispose();
}
public void Dispose()
{
foreach (var handle in Values)
handle.Dispose();
Clear();
}
/// <summary> Get the requested resource either from the cached resource, or load a new one if it does not exist. </summary>
public SafeResourceHandle Get(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData data)
{
if (TryGetClonedValue(path, out var handle))
return handle;
handle = loader.LoadResolvedSafeResource(category, type, path.Path, data);
var clone = handle.Clone();
if (!TryAdd(path, clone))
clone.Dispose();
return handle;
}
/// <summary> Get a cloned cached resource if it exists. </summary>
private bool TryGetClonedValue(Utf8GamePath path, [NotNullWhen(true)] out SafeResourceHandle? handle)
{
if (!TryGetValue(path, out handle))
return false;
handle = handle.Clone();
return true;
}
}

View file

@ -1,97 +1,54 @@
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public readonly struct EqdpCache : IDisposable
public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqdpIdentifier, EqdpEntry>(manager, collection)
{
private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
private readonly List<EqdpManipulation> _eqdpManipulations = new();
private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries =
[];
public EqdpCache()
{ }
public void SetFiles(MetaFileManager manager)
{
for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i)
manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
{
var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
if (i != -1)
manager.SetFile(_eqdpFiles[i], index);
}
public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory)
{
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
if (idx < 0)
{
Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
return null;
}
var i = CharacterUtilityData.EqdpIndices.IndexOf(idx);
if (i < 0)
{
Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
return null;
}
return manager.TemporarilySetFile(_eqdpFiles[i], idx);
}
public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry)
=> _fullEntries.TryGetValue((id, genderRace, accessory), out var pair)
? (originalEntry & pair.InverseMask) | pair.Entry
: originalEntry;
public void Reset()
{
foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>())
{
var relevant = CharacterUtility.RelevantIndices[file.Index.Value];
file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId));
}
_eqdpManipulations.Clear();
Clear();
_fullEntries.Clear();
}
public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry)
{
_eqdpManipulations.AddOrReplace(manip);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??=
new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar
return manip.Apply(file);
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
var mask = Eqdp.Mask(identifier.Slot);
var inverseMask = ~mask;
if (_fullEntries.TryGetValue(tuple, out var pair))
pair = ((pair.Entry & inverseMask) | (entry & mask), pair.InverseMask & inverseMask);
else
pair = (entry & mask, inverseMask);
_fullEntries[tuple] = pair;
}
public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
protected override void RevertModInternal(EqdpIdentifier identifier)
{
if (!_eqdpManipulations.Remove(manip))
return false;
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!;
manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
return manip.Apply(file);
if (!_fullEntries.Remove(tuple, out var pair))
return;
var mask = Eqdp.Mask(identifier.Slot);
var newMask = pair.InverseMask | mask;
if (newMask is not EqdpEntry.FullMask)
_fullEntries[tuple] = (pair.Entry & ~mask, newMask);
}
public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory)
=> _eqdpFiles
[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
public void Dispose()
protected override void Dispose(bool _)
{
for (var i = 0; i < _eqdpFiles.Length; ++i)
{
_eqdpFiles[i]?.Dispose();
_eqdpFiles[i] = null;
}
_eqdpManipulations.Clear();
Clear();
_fullEntries.Clear();
}
}

View file

@ -1,60 +1,66 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EqpCache : IDisposable
public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqpIdentifier, EqpEntry>(manager, collection)
{
private ExpandedEqpFile? _eqpFile = null;
private readonly List<EqpManipulation> _eqpManipulations = new();
public unsafe EqpEntry GetValues(CharacterArmor* armor)
{
var bodyEntry = GetSingleValue(armor[1].Set, EquipSlot.Body);
var headEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHead)
? GetSingleValue(armor[0].Set, EquipSlot.Head)
: GetSingleValue(armor[1].Set, EquipSlot.Head);
var handEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHand)
? GetSingleValue(armor[2].Set, EquipSlot.Hands)
: GetSingleValue(armor[1].Set, EquipSlot.Hands);
var (legsEntry, legsId) = bodyEntry.HasFlag(EqpEntry.BodyShowLeg)
? (GetSingleValue(armor[3].Set, EquipSlot.Legs), 3)
: (GetSingleValue(armor[1].Set, EquipSlot.Legs), 1);
var footEntry = legsEntry.HasFlag(EqpEntry.LegsShowFoot)
? GetSingleValue(armor[4].Set, EquipSlot.Feet)
: GetSingleValue(armor[legsId].Set, EquipSlot.Feet);
public EqpCache()
{ }
var combined = bodyEntry | headEntry | handEntry | legsEntry | footEntry;
return PostProcessFeet(PostProcessHands(combined));
}
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_eqpFile, MetaIndex.Eqp);
public static void ResetFiles(MetaFileManager manager)
=> manager.SetFile(null, MetaIndex.Eqp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot)
=> TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot);
public void Reset()
{
if (_eqpFile == null)
return;
=> Clear();
_eqpFile.Reset(_eqpManipulations.Select(m => m.SetId));
_eqpManipulations.Clear();
protected override void Dispose(bool _)
=> Clear();
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static EqpEntry PostProcessHands(EqpEntry entry)
{
if (!entry.HasFlag(EqpEntry.HandsHideForearm))
return entry;
var testFlag = entry.HasFlag(EqpEntry.HandsHideElbow)
? entry.HasFlag(EqpEntry.BodyHideGlovesL)
: entry.HasFlag(EqpEntry.BodyHideGlovesM);
return testFlag
? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS
: entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS);
}
public bool ApplyMod(MetaFileManager manager, EqpManipulation manip)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static EqpEntry PostProcessFeet(EqpEntry entry)
{
_eqpManipulations.AddOrReplace(manip);
_eqpFile ??= new ExpandedEqpFile(manager);
return manip.Apply(_eqpFile);
}
if (!entry.HasFlag(EqpEntry.FeetHideCalf))
return entry;
public bool RevertMod(MetaFileManager manager, EqpManipulation manip)
{
var idx = _eqpManipulations.FindIndex(manip.Equals);
if (idx < 0)
return false;
if (entry.HasFlag(EqpEntry.FeetHideKnee) || !entry.HasFlag(EqpEntry._20))
return entry & ~(EqpEntry.LegsHideBootsS | EqpEntry.LegsHideBootsM);
var def = ExpandedEqpFile.GetDefault(manager, manip.SetId);
manip = new EqpManipulation(def, manip.Slot, manip.SetId);
return manip.Apply(_eqpFile!);
}
public void Dispose()
{
_eqpFile?.Dispose();
_eqpFile = null;
_eqpManipulations.Clear();
return (entry | EqpEntry.LegsHideBootsM) & ~EqpEntry.LegsHideBootsS;
}
}

View file

@ -1,138 +1,19 @@
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EstCache : IDisposable
public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EstIdentifier, EstEntry>(manager, collection)
{
private EstFile? _estFaceFile = null;
private EstFile? _estHairFile = null;
private EstFile? _estBodyFile = null;
private EstFile? _estHeadFile = null;
private readonly List<EstManipulation> _estManipulations = new();
public EstCache()
{ }
public void SetFiles(MetaFileManager manager)
{
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
manager.SetFile(_estHairFile, MetaIndex.HairEst);
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
{
switch (index)
{
case MetaIndex.FaceEst:
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
break;
case MetaIndex.HairEst:
manager.SetFile(_estHairFile, MetaIndex.HairEst);
break;
case MetaIndex.BodyEst:
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
break;
case MetaIndex.HeadEst:
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
break;
}
}
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type)
{
var (file, idx) = type switch
{
EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst),
EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst),
EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst),
_ => (null, 0),
};
return manager.TemporarilySetFile(file, idx);
}
private readonly EstFile? GetEstFile(EstManipulation.EstType type)
{
return type switch
{
EstManipulation.EstType.Face => _estFaceFile,
EstManipulation.EstType.Hair => _estHairFile,
EstManipulation.EstType.Body => _estBodyFile,
EstManipulation.EstType.Head => _estHeadFile,
_ => null,
};
}
internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId)
{
var file = GetEstFile(type);
return file != null
? file[genderRace, primaryId.Id]
: EstFile.GetDefault(manager, type, genderRace, primaryId);
}
public EstEntry GetEstEntry(EstIdentifier identifier)
=> TryGetValue(identifier, out var entry)
? entry.Entry
: EstFile.GetDefault(Manager, identifier);
public void Reset()
{
_estFaceFile?.Reset();
_estHairFile?.Reset();
_estBodyFile?.Reset();
_estHeadFile?.Reset();
_estManipulations.Clear();
}
=> Clear();
public bool ApplyMod(MetaFileManager manager, EstManipulation m)
{
_estManipulations.AddOrReplace(m);
var file = m.Slot switch
{
EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair),
EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face),
EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body),
EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head),
_ => throw new ArgumentOutOfRangeException(),
};
return m.Apply(file);
}
public bool RevertMod(MetaFileManager manager, EstManipulation m)
{
if (!_estManipulations.Remove(m))
return false;
var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId);
var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def);
var file = m.Slot switch
{
EstManipulation.EstType.Hair => _estHairFile!,
EstManipulation.EstType.Face => _estFaceFile!,
EstManipulation.EstType.Body => _estBodyFile!,
EstManipulation.EstType.Head => _estHeadFile!,
_ => throw new ArgumentOutOfRangeException(),
};
return manip.Apply(file);
}
public void Dispose()
{
_estFaceFile?.Dispose();
_estHairFile?.Dispose();
_estBodyFile?.Dispose();
_estHeadFile?.Dispose();
_estFaceFile = null;
_estHairFile = null;
_estBodyFile = null;
_estHeadFile = null;
_estManipulations.Clear();
}
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -0,0 +1,122 @@
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>, IService
{
private readonly HashSet<PrimaryId> _doNotHideEarrings = [];
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
private readonly HashSet<PrimaryId> _doNotHideBracelets = [];
private readonly HashSet<PrimaryId> _doNotHideRingL = [];
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
private bool _doNotHideVieraHats;
private bool _doNotHideHrothgarHats;
private bool _hideAuRaHorns;
private bool _hideVieraEars;
private bool _hideMiqoteEars;
public new void Clear()
{
base.Clear();
_doNotHideEarrings.Clear();
_doNotHideNecklace.Clear();
_doNotHideBracelets.Clear();
_doNotHideRingL.Clear();
_doNotHideRingR.Clear();
_doNotHideHrothgarHats = false;
_doNotHideVieraHats = false;
_hideAuRaHorns = false;
_hideVieraEars = false;
_hideMiqoteEars = false;
}
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
{
if (Count == 0)
return original;
if (_doNotHideVieraHats)
original |= EqpEntry.HeadShowVieraHat;
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;
if (_doNotHideNecklace.Contains(armor[6].Set))
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
if (_doNotHideBracelets.Contains(armor[7].Set))
original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet;
if (_doNotHideRingR.Contains(armor[8].Set))
original |= EqpEntry.HandShowRingR;
if (_doNotHideRingL.Contains(armor[9].Set))
original |= EqpEntry.HandShowRingL;
return original;
}
public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation)
{
if (Remove(manipulation, out var oldMod) && oldMod == mod)
return false;
this[manipulation] = mod;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition),
GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Add(manipulation.Condition),
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition),
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;
}
public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod)
{
if (!Remove(manipulation, out mod))
return false;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition),
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

@ -1,56 +1,14 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct GmpCache : IDisposable
public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<GmpIdentifier, GmpEntry>(manager, collection)
{
private ExpandedGmpFile? _gmpFile = null;
private readonly List<GmpManipulation> _gmpManipulations = new();
public GmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_gmpFile, MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp);
public void Reset()
{
if (_gmpFile == null)
return;
=> Clear();
_gmpFile.Reset(_gmpManipulations.Select(m => m.SetId));
_gmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, GmpManipulation manip)
{
_gmpManipulations.AddOrReplace(manip);
_gmpFile ??= new ExpandedGmpFile(manager);
return manip.Apply(_gmpFile);
}
public bool RevertMod(MetaFileManager manager, GmpManipulation manip)
{
if (!_gmpManipulations.Remove(manip))
return false;
var def = ExpandedGmpFile.GetDefault(manager, manip.SetId);
manip = new GmpManipulation(def, manip.SetId);
return manip.Apply(_gmpFile!);
}
public void Dispose()
{
_gmpFile?.Dispose();
_gmpFile = null;
_gmpManipulations.Clear();
}
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -1,123 +1,102 @@
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.String;
namespace Penumbra.Collections.Cache;
public readonly struct ImcCache : IDisposable
public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ImcIdentifier, ImcEntry>(manager, collection)
{
private readonly Dictionary<Utf8GamePath, ImcFile> _imcFiles = new();
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new();
private readonly Dictionary<CiByteString, (ImcFile, HashSet<ImcIdentifier>)> _imcFiles = [];
public ImcCache()
{ }
public bool HasFile(CiByteString path)
=> _imcFiles.ContainsKey(path);
public void SetFiles(ModCollection collection, bool fromFullCompute)
public bool GetFile(CiByteString path, [NotNullWhen(true)] out ImcFile? file)
{
if (fromFullCompute)
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFileSync(path, CreateImcPath(collection, path));
else
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFile(path, CreateImcPath(collection, path));
}
public void Reset(ModCollection collection)
{
foreach (var (path, file) in _imcFiles)
if (!_imcFiles.TryGetValue(path, out var p))
{
collection._cache!.RemovePath(path);
file.Reset();
}
_imcManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip)
{
if (!manip.Validate())
file = null;
return false;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip));
if (idx < 0)
{
idx = _imcManipulations.Count;
_imcManipulations.Add((manip, null!));
}
var path = manip.GamePath();
file = p.Item1;
return true;
}
public void Reset()
{
foreach (var (_, (file, set)) in _imcFiles)
{
file.Reset();
set.Clear();
}
_imcFiles.Clear();
Clear();
}
protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry)
{
Collection.Counters.IncrementImc();
ApplyFile(identifier, entry);
}
private void ApplyFile(ImcIdentifier identifier, ImcEntry entry)
{
var path = identifier.GamePath().Path;
try
{
if (!_imcFiles.TryGetValue(path, out var file))
file = new ImcFile(manager, manip);
if (!_imcFiles.TryGetValue(path, out var pair))
pair = (new ImcFile(Manager, identifier), []);
_imcManipulations[idx] = (manip, file);
if (!manip.Apply(file))
return false;
if (!Apply(pair.Item1, identifier, entry))
return;
_imcFiles[path] = file;
var fullPath = CreateImcPath(collection, path);
collection._cache!.ForceFile(path, fullPath);
return true;
pair.Item2.Add(identifier);
_imcFiles[path] = pair;
}
catch (ImcException e)
{
manager.ValidityChecker.ImcExceptions.Add(e);
Manager.ValidityChecker.ImcExceptions.Add(e);
Penumbra.Log.Error(e.ToString());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}");
Penumbra.Log.Error($"Could not apply IMC Manipulation {identifier}:\n{e}");
}
return false;
}
public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m)
protected override void RevertModInternal(ImcIdentifier identifier)
{
if (!m.Validate())
return false;
Collection.Counters.IncrementImc();
var path = identifier.GamePath().Path;
if (!_imcFiles.TryGetValue(path, out var pair))
return;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m));
if (idx < 0)
return false;
if (!pair.Item2.Remove(identifier))
return;
var (_, file) = _imcManipulations[idx];
_imcManipulations.RemoveAt(idx);
if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file)))
if (pair.Item2.Count == 0)
{
_imcFiles.Remove(file.Path);
collection._cache!.ForceFile(file.Path, FullPath.Empty);
file.Dispose();
return true;
_imcFiles.Remove(path);
pair.Item1.Dispose();
return;
}
var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _);
var manip = m.Copy(def);
if (!manip.Apply(file))
return false;
var fullPath = CreateImcPath(collection, file.Path);
collection._cache!.ForceFile(file.Path, fullPath);
return true;
var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _);
Apply(pair.Item1, identifier, def);
}
public void Dispose()
public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry)
=> file.SetEntry(ImcFile.PartIndex(identifier.EquipSlot), identifier.Variant.Id, entry);
protected override void Dispose(bool _)
{
foreach (var file in _imcFiles.Values)
foreach (var (_, (file, _)) in _imcFiles)
file.Dispose();
Clear();
_imcFiles.Clear();
_imcManipulations.Clear();
}
private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path)
=> new($"|{collection.Name}_{collection.ChangeCounter}|{path}");
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
=> _imcFiles.TryGetValue(path, out file);
}

View file

@ -1,234 +1,137 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>>
public class MetaCache(MetaFileManager manager, ModCollection collection)
{
private readonly MetaFileManager _manager;
private readonly ModCollection _collection;
private readonly Dictionary<MetaManipulation, IMod> _manipulations = new();
private EqpCache _eqpCache = new();
private readonly EqdpCache _eqdpCache = new();
private EstCache _estCache = new();
private GmpCache _gmpCache = new();
private CmpCache _cmpCache = new();
private readonly ImcCache _imcCache = new();
public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
return _manipulations.TryGetValue(manip, out mod);
}
}
public readonly EqpCache Eqp = new(manager, collection);
public readonly EqdpCache Eqdp = new(manager, collection);
public readonly EstCache Est = new(manager, 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
=> _manipulations.Count;
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
public IReadOnlyCollection<MetaManipulation> Manipulations
=> _manipulations.Keys;
public IEnumerator<KeyValuePair<MetaManipulation, IMod>> GetEnumerator()
=> _manipulations.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public MetaCache(MetaFileManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
if (!_manager.CharacterUtility.Ready)
_manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
}
public void SetFiles()
{
_eqpCache.SetFiles(_manager);
_eqdpCache.SetFiles(_manager);
_estCache.SetFiles(_manager);
_gmpCache.SetFiles(_manager);
_cmpCache.SetFiles(_manager);
_imcCache.SetFiles(_collection, false);
}
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
.Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.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()
{
_eqpCache.Reset();
_eqdpCache.Reset();
_estCache.Reset();
_gmpCache.Reset();
_cmpCache.Reset();
_imcCache.Reset(_collection);
_manipulations.Clear();
Eqp.Reset();
Eqdp.Reset();
Est.Reset();
Gmp.Reset();
Rsp.Reset();
Imc.Reset();
Atch.Reset();
Shp.Reset();
Atr.Reset();
GlobalEqp.Clear();
}
public void Dispose()
{
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
_eqpCache.Dispose();
_eqdpCache.Dispose();
_estCache.Dispose();
_gmpCache.Dispose();
_cmpCache.Dispose();
_imcCache.Dispose();
_manipulations.Clear();
if (IsDisposed)
return;
IsDisposed = true;
Eqp.Dispose();
Eqdp.Dispose();
Est.Dispose();
Gmp.Dispose();
Rsp.Dispose();
Imc.Dispose();
Atch.Dispose();
Shp.Dispose();
Atr.Dispose();
}
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
mod = null;
return identifier switch
{
EqdpIdentifier i => Eqdp.TryGetValue(i, out var p) && Convert(p, out mod),
EqpIdentifier i => Eqp.TryGetValue(i, out var p) && Convert(p, out mod),
EstIdentifier i => Est.TryGetValue(i, out var p) && Convert(p, out mod),
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,
};
static bool Convert<T>((IMod, T) pair, out IMod mod)
{
mod = pair.Item1;
return true;
}
}
public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
=> identifier switch
{
EqdpIdentifier i => Eqdp.RevertMod(i, out mod),
EqpIdentifier i => Eqp.RevertMod(i, out mod),
EstIdentifier i => Est.RevertMod(i, out mod),
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,
};
public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry)
=> identifier switch
{
EqdpIdentifier i when entry is EqdpEntry e => Eqdp.ApplyMod(mod, i, e),
EqdpIdentifier i when entry is EqdpEntryInternal e => Eqdp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EqpIdentifier i when entry is EqpEntry e => Eqp.ApplyMod(mod, i, e),
EqpIdentifier i when entry is EqpEntryInternal e => Eqp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EstIdentifier i when entry is EstEntry e => Est.ApplyMod(mod, i, e),
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,
};
~MetaCache()
=> Dispose();
public bool ApplyMod(MetaManipulation manip, IMod mod)
{
lock (_manipulations)
{
if (_manipulations.ContainsKey(manip))
_manipulations.Remove(manip);
_manipulations[manip] = mod;
}
if (!_manager.CharacterUtility.Ready)
return true;
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
var ret = _manipulations.Remove(manip, out mod);
if (!_manager.CharacterUtility.Ready)
return ret;
}
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
/// <summary> Set a single file. </summary>
public void SetFile(MetaIndex metaIndex)
{
switch (metaIndex)
{
case MetaIndex.Eqp:
_eqpCache.SetFiles(_manager);
break;
case MetaIndex.Gmp:
_gmpCache.SetFiles(_manager);
break;
case MetaIndex.HumanCmp:
_cmpCache.SetFiles(_manager);
break;
case MetaIndex.FaceEst:
case MetaIndex.HairEst:
case MetaIndex.HeadEst:
case MetaIndex.BodyEst:
_estCache.SetFile(_manager, metaIndex);
break;
default:
_eqdpCache.SetFile(_manager, metaIndex);
break;
}
}
/// <summary> Set the currently relevant IMC files for the collection cache. </summary>
public void SetImcFiles(bool fromFullCompute)
=> _imcCache.SetFiles(_collection, fromFullCompute);
public MetaList.MetaReverter TemporarilySetEqpFile()
=> _eqpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory);
public MetaList.MetaReverter TemporarilySetGmpFile()
=> _gmpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter TemporarilySetCmpFile()
=> _cmpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type)
=> _estCache.TemporarilySetFiles(_manager, type);
/// <summary> Try to obtain a manipulated IMC file. </summary>
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
=> _imcCache.GetImcFile(path, out file);
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId)
{
var eqdpFile = _eqdpCache.EqdpFile(race, accessory);
if (eqdpFile != null)
return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default;
else
return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId);
}
=> Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId));
internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId)
=> _estCache.GetEstEntry(_manager, type, genderRace, primaryId);
/// <summary> Use this when CharacterUtility becomes ready. </summary>
private void ApplyStoredManipulations()
{
if (!_manager.CharacterUtility.Ready)
return;
var loaded = 0;
lock (_manipulations)
{
foreach (var manip in Manipulations)
{
loaded += manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
}
? 1
: 0;
}
}
_manager.ApplyDefaultFiles(_collection);
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations.");
}
internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
=> Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace));
}

View file

@ -0,0 +1,47 @@
using OtterGui.Classes;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager, ModCollection collection)
: ReadWriteDictionary<TIdentifier, (IMod Source, TEntry Entry)>
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
protected readonly MetaFileManager Manager = manager;
protected readonly ModCollection Collection = collection;
public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry)
{
if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer<TEntry>.Default.Equals(pair.Entry, entry))
return false;
this[identifier] = (source, entry);
ApplyModInternal(identifier, entry);
return true;
}
public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
if (!Remove(identifier, out var pair))
{
mod = null;
return false;
}
mod = pair.Source;
RevertModInternal(identifier);
return true;
}
protected virtual void ApplyModInternal(TIdentifier identifier, TEntry entry)
{ }
protected virtual void RevertModInternal(TIdentifier identifier)
{ }
}

View file

@ -0,0 +1,13 @@
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<RspIdentifier, RspEntry>(manager, collection)
{
public void Reset()
=> Clear();
protected override void Dispose(bool _)
=> Clear();
}

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

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
@ -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,9 @@
using Dalamud.Interface.Internal.Notifications;
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;
using Penumbra.GameData.Enums;
@ -11,7 +12,7 @@ using Penumbra.UI;
namespace Penumbra.Collections.Manager;
public class ActiveCollectionData
public class ActiveCollectionData : IService
{
public ModCollection Current { get; internal set; } = ModCollection.Empty;
public ModCollection Default { get; internal set; } = ModCollection.Empty;
@ -20,9 +21,9 @@ public class ActiveCollectionData
public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
}
public class ActiveCollections : ISavable, IDisposable
public class ActiveCollections : ISavable, IDisposable, IService
{
public const int Version = 1;
public const int Version = 2;
private readonly CollectionStorage _storage;
private readonly CommunicatorService _communicator;
@ -218,7 +219,7 @@ public class ActiveCollections : ISavable, IDisposable
_ => null,
};
if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count)
if (oldCollection == null || collection == oldCollection || collection.Identity.Index >= _storage.Count)
return;
switch (collectionType)
@ -261,16 +262,17 @@ public class ActiveCollections : ISavable, IDisposable
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Name },
{ nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name },
{ 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.Name);
jObj.Add(type.ToString(), collection.Identity.Id);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
jObj.WriteTo(j);
}
@ -280,7 +282,7 @@ public class ActiveCollections : ISavable, IDisposable
.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)
@ -298,7 +300,7 @@ public class ActiveCollections : ISavable, IDisposable
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)
{
@ -319,22 +321,16 @@ public class ActiveCollections : ISavable, IDisposable
}
}
/// <summary>
/// Load default, current, special, and character collections from config.
/// If a collection does not exist anymore, reset it to an appropriate default.
/// </summary>
private void LoadCollections()
private bool LoadCollectionsV1(JObject jObject)
{
Penumbra.Log.Debug("[Collections] Reading collection assignments...");
var configChanged = !Load(_saveService.FileNames, out var jObject);
// Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed.
var defaultName = jObject[nameof(Default)]?.ToObject<string>()
?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name);
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.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}.", NotificationType.Warning);
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
}
@ -344,11 +340,12 @@ public class ActiveCollections : ISavable, IDisposable
}
// 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}.", NotificationType.Warning);
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
@ -358,11 +355,12 @@ public class ActiveCollections : ISavable, IDisposable
}
// 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}.", NotificationType.Warning);
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
}
@ -393,11 +391,124 @@ public class ActiveCollections : ISavable, IDisposable
Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments.");
configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 1);
// Save any changes.
if (configChanged)
_saveService.ImmediateSave(this);
return configChanged;
}
private bool LoadCollectionsV2(JObject jObject)
{
var configChanged = false;
// Load the default collection. If the guid does not exist take the empty collection.
var defaultId = jObject[nameof(Default)]?.ToObject<Guid>() ?? Guid.Empty;
if (!_storage.ById(defaultId, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
}
else
{
Default = defaultCollection;
}
// 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.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.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
else
{
Interface = interfaceCollection;
}
// Load the current collection.
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 {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
}
else
{
Current = currentCollection;
}
// Load special collections.
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{
var typeId = jObject[type.ToString()]?.ToObject<Guid>();
if (typeId == null)
continue;
if (!_storage.ById(typeId.Value, out var typeCollection))
{
Penumbra.Messager.NotificationMessage($"Last choice of {name} Collection {typeId.Value} is not available, removed.",
NotificationType.Warning);
configChanged = true;
}
else
{
SpecialCollections[(int)type] = typeCollection;
}
}
Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments.");
configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 2);
return configChanged;
}
private bool LoadCollectionsNew()
{
Current = _storage.DefaultNamed;
Default = _storage.DefaultNamed;
Interface = _storage.DefaultNamed;
return true;
}
/// <summary>
/// Load default, current, special, and character collections from config.
/// If a collection does not exist anymore, reset it to an appropriate default.
/// </summary>
private void LoadCollections()
{
Penumbra.Log.Debug("[Collections] Reading collection assignments...");
var configChanged = !Load(_saveService.FileNames, out var jObject);
var version = jObject["Version"]?.ToObject<int>() ?? 0;
var changed = false;
switch (version)
{
case 1:
changed = LoadCollectionsV1(jObject);
break;
case 2:
changed = LoadCollectionsV2(jObject);
break;
case 0 when configChanged:
changed = LoadCollectionsNew();
break;
case 0:
Penumbra.Messager.NotificationMessage("Active Collections File has unknown version and will be reset.",
NotificationType.Warning);
changed = LoadCollectionsNew();
break;
}
if (changed)
_saveService.ImmediateSaveSync(this);
}
/// <summary>
@ -410,7 +521,7 @@ public class ActiveCollections : ISavable, IDisposable
var jObj = BackupService.GetJObjectForFile(fileNames, file);
if (jObj == null)
{
ret = new JObject();
ret = [];
return false;
}
@ -476,7 +587,7 @@ public class ActiveCollections : ISavable, IDisposable
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;
}
@ -485,12 +596,12 @@ public class ActiveCollections : ISavable, IDisposable
{
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;
}
@ -506,7 +617,7 @@ public class ActiveCollections : ISavable, IDisposable
if (maleNpc == null)
{
maleNpc = Default;
if (maleNpc.Index != checkAssignment.Index)
if (maleNpc.Identity.Index != checkAssignment.Identity.Index)
return string.Empty;
collection1 = CollectionType.Default;
@ -515,7 +626,7 @@ public class ActiveCollections : ISavable, IDisposable
if (femaleNpc == null)
{
femaleNpc = Default;
if (femaleNpc.Index != checkAssignment.Index)
if (femaleNpc.Identity.Index != checkAssignment.Identity.Index)
return string.Empty;
collection2 = CollectionType.Default;
@ -535,7 +646,7 @@ public class ActiveCollections : ISavable, IDisposable
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,32 +1,22 @@
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public class CollectionEditor
public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
public CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage)
{
_saveService = saveService;
_communicator = communicator;
_modStorage = modStorage;
}
/// <summary> Enable or disable the mod inheritance of mod idx. </summary>
public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit)
{
if (!FixInheritance(collection, mod, inherit))
return false;
InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? 0 : 1, 0);
InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? Setting.False : Setting.True, 0);
return true;
}
@ -36,13 +26,14 @@ public class CollectionEditor
/// </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;
InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? -1 : newValue ? 0 : 1, 0);
collection.GetOwnSettings(mod.Index)!.Enabled = newValue;
InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True,
0);
return true;
}
@ -52,7 +43,7 @@ public class CollectionEditor
if (!mods.Aggregate(false, (current, mod) => current | FixInheritance(collection, mod, inherit)))
return;
InvokeChange(collection, ModSettingChange.MultiInheritance, null, -1, 0);
InvokeChange(collection, ModSettingChange.MultiInheritance, null, Setting.Indefinite, 0);
}
/// <summary>
@ -64,56 +55,85 @@ public class CollectionEditor
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)
return;
InvokeChange(collection, ModSettingChange.MultiEnableState, null, -1, 0);
InvokeChange(collection, ModSettingChange.MultiEnableState, null, Setting.Indefinite, 0);
}
/// <summary>
/// Set the priority of mod idx to newValue if it differs from the current priority.
/// If the mod is currently inherited, stop the inheritance.
/// </summary>
public bool SetModPriority(ModCollection collection, Mod mod, int newValue)
public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue)
{
var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? 0;
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;
InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? -1 : oldValue, 0);
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, uint newValue)
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);
InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? -1 : (int)oldValue, groupIdx);
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)
{
@ -124,10 +144,10 @@ public class CollectionEditor
// 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;
@ -157,75 +177,59 @@ public class CollectionEditor
// or remove any unused settings for the target if they are inheriting.
if (savedSettings != null)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings)[targetName] = savedSettings.Value;
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
((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));
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
}
return true;
}
/// <summary>
/// Change one of the available mod settings for mod idx discerned by type.
/// If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored.
/// The setting will also be automatically fixed if it is invalid for that setting group.
/// For boolean parameters, newValue == 0 will be treated as false and != 0 as true.
/// </summary>
public bool ChangeModSetting(ModCollection collection, ModSettingChange type, Mod mod, int newValue, int groupIdx)
{
return type switch
{
ModSettingChange.Inheritance => SetModInheritance(collection, mod, newValue != 0),
ModSettingChange.EnableState => SetModState(collection, mod, newValue != 0),
ModSettingChange.Priority => SetModPriority(collection, mod, newValue),
ModSettingChange.Setting => SetModSetting(collection, mod, groupIdx, (uint)newValue),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
/// <summary>
/// Set inheritance of a mod without saving,
/// to be used as an intermediary.
/// </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;
}
/// <summary> Queue saves and trigger changes for any non-inherited change in a collection, then trigger changes for all inheritors. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx)
private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
_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)
saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false);
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, int oldValue, int groupIdx)
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)
{
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
_communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true);
communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true);
break;
default:
if (directInheritor.Settings[mod!.Index] == null)
_communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true);
if (directInheritor.GetOwnSettings(mod!.Index) == null)
communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true);
break;
}

View file

@ -1,3 +1,4 @@
using OtterGui.Services;
using Penumbra.Collections.Cache;
namespace Penumbra.Collections.Manager;
@ -8,7 +9,7 @@ public class CollectionManager(
InheritanceManager inheritances,
CollectionCacheManager caches,
TempCollectionManager temp,
CollectionEditor editor)
CollectionEditor editor) : IService
{
public readonly CollectionStorage Storage = storage;
public readonly ActiveCollections Active = active;

View file

@ -1,30 +1,80 @@
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
{
public static readonly LocalCollectionId Zero = new(0);
public static LocalCollectionId operator +(LocalCollectionId left, int right)
=> new(left.Id + right);
}
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
public ModCollection Create(string name, int index, ModCollection? duplicate)
{
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> inheritances)
{
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
{
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public void Delete(ModCollection collection)
=> _collectionsByLocal.Remove(collection.Identity.LocalId);
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections =
[
ModCollection.Empty,
];
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
private readonly Dictionary<LocalCollectionId, ModCollection>
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
public readonly ModCollection DefaultNamed;
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
/// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
@ -42,12 +92,35 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
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;
}
/// <summary> Find a collection by its id. If the GUID is empty, the empty collection is returned. </summary>
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
{
if (id != Guid.Empty)
return _collections.FindFirst(c => c.Identity.Id == id, out collection);
collection = ModCollection.Empty;
return true;
}
/// <summary> Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. </summary>
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
{
if (Guid.TryParse(identifier, out var guid))
return ById(guid, out collection);
return ByName(identifier, out collection);
}
/// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary>
public ModCollection ByLocalId(LocalCollectionId localId)
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
@ -70,31 +143,6 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
}
/// <summary>
/// Returns true if the name is not empty, it is not the name of the empty collection
/// and no existing collection results in the same filename as name. Also returns the fixed name.
/// </summary>
public bool CanAddCollection(string name, out string fixedName)
{
if (!IsValidName(name))
{
fixedName = string.Empty;
return false;
}
name = name.ToLowerInvariant();
if (name.Length == 0
|| name == ModCollection.Empty.Name.ToLowerInvariant()
|| _collections.Any(c => c.Name.ToLowerInvariant() == name))
{
fixedName = string.Empty;
return false;
}
fixedName = name;
return true;
}
/// <summary>
/// Add a new collection of the given name.
/// If duplicate is not-null, the new collection will be a duplicate of it.
@ -104,20 +152,13 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
if (!CanAddCollection(name, out var fixedName))
{
Penumbra.Messager.NotificationMessage(
$"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning,
false);
if (name.Length == 0)
return false;
}
var newCollection = duplicate?.Duplicate(name, _collections.Count)
?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
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;
}
@ -127,55 +168,54 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// </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;
}
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;
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));
}
/// <summary>
/// Check if a name is valid to use for a collection.
/// Does not check for uniqueness.
/// </summary>
private static bool IsValidName(string name)
=> name.Length is > 0 and < 64 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath());
/// <summary>
/// Read all collection files in the Collection Directory.
/// Ensure that the default named collection exists, and apply inheritances afterwards.
/// Ensure that the default named collection exists, and apply inheritances afterward.
/// Duplicate collection files are not deleted, just not added here.
/// </summary>
private void ReadCollections(out ModCollection defaultNamedCollection)
@ -183,29 +223,64 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
Penumbra.Log.Debug("[Collections] Reading saved collections...");
foreach (var file in _saveService.FileNames.CollectionFiles)
{
if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance))
if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance))
continue;
if (!IsValidName(name))
if (id == Guid.Empty)
{
// TODO: handle better.
Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.",
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
continue;
}
if (ById(id, out _))
{
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
NotificationType.Warning);
continue;
}
if (ByName(name, out _))
{
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.",
NotificationType.Warning);
continue;
}
var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance);
var collection = CreateFromData(id, name, version, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.",
NotificationType.Warning);
try
{
if (version >= 2)
{
try
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage(
$"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.Identity.Identifier}, rename failed:\n{ex}",
NotificationType.Warning);
}
}
else
{
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
try
{
File.Move(file.FullName, file.FullName + ".bak", true);
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file.");
}
catch (Exception ex)
{
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}");
}
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
NotificationType.Error);
}
_collections.Add(collection);
}
@ -220,14 +295,14 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// </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];
}
@ -236,7 +311,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
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>
@ -244,7 +319,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
{
// 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>
@ -255,21 +330,22 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
{
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]?.FixAllSettings(mod) ?? false)
if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(mod.Index, null);
}
break;
@ -277,7 +353,8 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int movedToIdx)
{
type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)
@ -285,8 +362,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
foreach (var collection in this)
{
if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, 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);
}
}
@ -298,9 +376,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
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

@ -107,6 +107,9 @@ public static class CollectionTypeExtensions
public static bool IsSpecial(this CollectionType collectionType)
=> collectionType < CollectionType.Default;
public static bool CanBeRemoved(this CollectionType collectionType)
=> collectionType.IsSpecial() || collectionType is CollectionType.Individual;
public static readonly (CollectionType, string, string)[] Special = Enum.GetValues<CollectionType>()
.Where(IsSpecial)
.Select(s => (s, s.ToName(), s.ToDescription()))

View file

@ -48,8 +48,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
// Handle generic NPC
var npcIdentifier = _actors.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty,
ushort.MaxValue,
identifier.Kind, identifier.DataId);
ushort.MaxValue, identifier.Kind, identifier.DataId);
if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection))
return true;
@ -58,8 +57,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
return false;
identifier = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName,
identifier.HomeWorld.Id,
ObjectKind.None, uint.MaxValue);
identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue);
return CheckWorlds(identifier, out collection);
}
case IdentifierType.Npc: return _individuals.TryGetValue(identifier, out collection);
@ -127,7 +125,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
}
}
public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection)
public bool TryGetCollection(IGameObject? gameObject, out ModCollection? collection)
=> TryGetCollection(_actors.FromObject(gameObject, true, false, false), out collection);
public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection)

View file

@ -1,5 +1,5 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
@ -18,7 +18,7 @@ public partial class IndividualCollections
foreach (var (name, identifiers, collection) in Assignments)
{
var tmp = identifiers[0].ToJson();
tmp.Add("Collection", collection.Name);
tmp.Add("Collection", collection.Identity.Id);
tmp.Add("Display", name);
ret.Add(tmp);
}
@ -26,18 +26,28 @@ public partial class IndividualCollections
return ret;
}
public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage)
public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage, int version)
{
if (_actors.Awaiter.IsCompletedSuccessfully)
{
var ret = ReadJObjectInternal(obj, storage);
var ret = version switch
{
1 => ReadJObjectInternalV1(obj, storage),
2 => ReadJObjectInternalV2(obj, storage),
_ => true,
};
return ret;
}
Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready...");
_actors.Awaiter.ContinueWith(_ =>
{
if (ReadJObjectInternal(obj, storage))
if (version switch
{
1 => ReadJObjectInternalV1(obj, storage),
2 => ReadJObjectInternalV2(obj, storage),
_ => true,
})
saver.ImmediateSave(parent);
IsLoaded = true;
Loaded.Invoke();
@ -45,7 +55,55 @@ public partial class IndividualCollections
return false;
}
private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage)
private bool ReadJObjectInternalV1(JArray? obj, CollectionStorage storage)
{
Penumbra.Log.Debug("[Collections] Reading individual assignments...");
if (obj == null)
{
Penumbra.Log.Debug($"[Collections] Finished reading {Count} individual assignments...");
return true;
}
foreach (var data in obj)
{
try
{
var identifier = _actors.FromJson(data as JObject);
var group = GetGroup(identifier);
if (group.Length == 0 || group.Any(i => !i.IsValid))
{
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.",
NotificationType.Error);
continue;
}
var collectionName = data["Collection"]?.ToObject<string>() ?? string.Empty;
if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection))
{
Penumbra.Messager.NotificationMessage(
$"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.",
NotificationType.Warning);
continue;
}
if (!Add(group, collection))
{
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.",
NotificationType.Warning);
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error);
}
}
Penumbra.Log.Debug($"Finished reading {Count} individual assignments...");
return true;
}
private bool ReadJObjectInternalV2(JArray? obj, CollectionStorage storage)
{
Penumbra.Log.Debug("[Collections] Reading individual assignments...");
if (obj == null)
@ -64,17 +122,17 @@ public partial class IndividualCollections
if (group.Length == 0 || group.Any(i => !i.IsValid))
{
changes = true;
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.",
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed assignment.",
NotificationType.Error);
continue;
}
var collectionName = data["Collection"]?.ToObject<string>() ?? string.Empty;
if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection))
var collectionId = data["Collection"]?.ToObject<Guid>();
if (!collectionId.HasValue || !storage.ById(collectionId.Value, out var collection))
{
changes = true;
Penumbra.Messager.NotificationMessage(
$"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.",
$"Could not load the collection {collectionId} as individual collection for {identifier}, removed assignment.",
NotificationType.Warning);
continue;
}
@ -82,14 +140,14 @@ public partial class IndividualCollections
if (!Add(group, collection))
{
changes = true;
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.",
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed assignment.",
NotificationType.Warning);
}
}
catch (Exception e)
{
changes = true;
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error);
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed assignment.", NotificationType.Error);
}
}
@ -100,14 +158,6 @@ public partial class IndividualCollections
internal void Migrate0To1(Dictionary<string, ModCollection> old)
{
static bool FindDataId(string name, NameDictionary data, out NpcId dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<NpcId, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
foreach (var (name, collection) in old)
{
var kind = ObjectKind.None;
@ -132,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.
@ -142,18 +192,28 @@ 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);
}
}
return;
static bool FindDataId(string name, NameDictionary data, out NpcId dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<NpcId, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
}
}

View file

@ -1,12 +1,10 @@
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
@ -15,7 +13,7 @@ namespace Penumbra.Collections.Manager;
/// This is transitive, so a collection A inheriting from B also inherits from everything B inherits.
/// Circular dependencies are resolved by distinctness.
/// </summary>
public class InheritanceManager : IDisposable
public class InheritanceManager : IDisposable, IService
{
public enum ValidInheritance
{
@ -64,10 +62,10 @@ public class InheritanceManager : IDisposable
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;
@ -84,25 +82,23 @@ public class InheritanceManager : IDisposable
/// <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)"/>
@ -111,16 +107,16 @@ public class InheritanceManager : IDisposable
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;
}
@ -132,29 +128,42 @@ public class InheritanceManager : IDisposable
{
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 (_storage.ByName(subCollectionName, out var subCollection))
if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection))
{
if (AddInheritance(collection, subCollection, false))
continue;
changes = true;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
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.Identity.AnonymizedName} from name to GUID.");
if (AddInheritance(collection, subCollection, false))
continue;
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.", NotificationType.Warning);
$"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));
}
@ -167,20 +176,22 @@ public class InheritanceManager : IDisposable
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

@ -1,8 +1,6 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Settings;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
@ -28,21 +26,21 @@ 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;
}
/// <summary> We treat every completely defaulted setting as inheritance-ready. </summary>
private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting)
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0);
=> setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.Values.All(s => s == Setting.Zero);
/// <inheritdoc cref="SettingIsDefaultV0(ModSettings.SavedSettings)"/>
private static bool SettingIsDefaultV0(ModSettings? setting)
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0);
=> setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.All(s => s == Setting.Zero);
}

View file

@ -1,3 +1,5 @@
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
@ -7,15 +9,15 @@ using Penumbra.String;
namespace Penumbra.Collections.Manager;
public class TempCollectionManager : IDisposable
public class TempCollectionManager : IDisposable, IService
{
public int GlobalChangeCounter { get; private set; } = 0;
public int GlobalChangeCounter { get; private set; }
public readonly IndividualCollections Collections;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActorManager _actors;
private readonly Dictionary<string, ModCollection> _customCollections = new();
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActorManager _actors;
private readonly Dictionary<Guid, ModCollection> _customCollections = [];
public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorManager actors, CollectionStorage storage)
{
@ -42,37 +44,38 @@ public class TempCollectionManager : IDisposable
=> _customCollections.Values;
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(name.ToLowerInvariant(), out collection);
=> _customCollections.Values.FindFirst(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase), out collection);
public string CreateTemporaryCollection(string name)
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(id, out collection);
public Guid CreateTemporaryCollection(string name)
{
if (_storage.ByName(name, out _))
return string.Empty;
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++);
Penumbra.Log.Debug($"Creating temporary collection {collection.AnonymizedName}.");
if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection))
var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++);
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.Name;
return collection.Identity.Id;
}
return string.Empty;
return Guid.Empty;
}
public bool RemoveTemporaryCollection(string collectionName)
public bool RemoveTemporaryCollection(Guid collectionId)
{
if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection))
if (!_customCollections.Remove(collectionId, out var collection))
{
Penumbra.Log.Debug($"Tried to delete temporary collection {collectionName.ToLowerInvariant()}, but did not exist.");
Penumbra.Log.Debug($"Tried to delete temporary collection {collectionId}, but did not exist.");
return false;
}
Penumbra.Log.Debug($"Deleted temporary collection {collection.AnonymizedName}.");
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
_storage.Delete(collection);
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)
@ -80,7 +83,7 @@ public class TempCollectionManager : IDisposable
// Temporary collection assignment removed.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.AnonymizedName} from {Collections[i].DisplayName}.");
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Identity.Id} from {Collections[i].DisplayName}.");
Collections.Delete(i--);
}
@ -93,37 +96,37 @@ public class TempCollectionManager : IDisposable
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;
}
public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers)
public bool AddIdentifier(Guid collectionId, params ActorIdentifier[] identifiers)
{
if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection))
if (!_customCollections.TryGetValue(collectionId, out var collection))
return false;
return AddIdentifier(collection, identifiers);
}
public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue)
public bool AddIdentifier(Guid collectionId, string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
if (!ByteString.FromString(characterName, out var byteString))
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
if (!identifier.IsValid)
return false;
return AddIdentifier(collectionName, identifier);
return AddIdentifier(collectionId, identifier);
}
internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
if (!ByteString.FromString(characterName, out var byteString))
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Identity.Id);
}
}

View file

@ -1,12 +1,8 @@
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.Mods;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.Collections.Cache;
using Penumbra.Interop.Services;
using Penumbra.GameData.Data;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections;
@ -47,71 +43,15 @@ public partial class ModCollection
internal MetaCache? MetaCache
=> _cache?.Meta;
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
{
if (_cache != null)
return _cache.Meta.GetImcFile(path, out file);
file = null;
return false;
}
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, object?)>();
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>>();
internal SingleArray<ModConflicts> Conflicts(Mod mod)
=> _cache?.Conflicts(mod) ?? new SingleArray<ModConflicts>();
public void SetFiles(CharacterUtility utility)
{
if (_cache == null)
{
utility.ResetAll();
}
else
{
_cache.Meta.SetFiles();
Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}.");
}
}
public void SetMetaFile(CharacterUtility utility, MetaIndex idx)
{
if (_cache == null)
utility.ResetResource(idx);
else
_cache.Meta.SetFile(idx);
}
// Used for short periods of changed files.
public MetaList.MetaReverter? TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory)
{
if (_cache != null)
return _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory);
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
return idx >= 0 ? utility.TemporarilyResetResource(idx) : null;
}
public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetEqpFile()
?? utility.TemporarilyResetResource(MetaIndex.Eqp);
public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetGmpFile()
?? utility.TemporarilyResetResource(MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetCmpFile()
?? utility.TemporarilyResetResource(MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type)
=> _cache?.Meta.TemporarilySetEstFile(type)
?? utility.TemporarilyResetResource((MetaIndex)type);
}

View file

@ -1,7 +1,6 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections;
@ -13,196 +12,129 @@ 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 = 1;
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 = CreateEmpty(EmptyCollectionName, 0, 0);
public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, new ModSettingProvider(),
new ModCollectionInheritance());
/// <summary> The name of a collection can not contain characters invalid in a path. </summary>
public string Name { get; internal init; }
public ModCollectionIdentity Identity;
public override string ToString()
=> Name;
=> 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.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
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; }
/// <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.
/// </summary>
public ModCollection Duplicate(string name, int index)
public ModCollection Duplicate(string name, LocalCollectionId localId, int index)
{
Debug.Assert(index > 0, "Collection duplicated with non-positive index.");
return new ModCollection(name, 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, string name, 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(name, index, 0, version, new List<ModSettings?>(), new List<ModCollection>(), 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;
}
/// <summary> Constructor for temporary collections. </summary>
public static ModCollection CreateTemporary(string name, int index, int changeCounter)
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(name, index, changeCounter, CurrentVersion, new List<ModSettings?>(), new List<ModCollection>(),
new Dictionary<string, ModSettings.SavedSettings>());
var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, new ModSettingProvider(),
new ModCollectionInheritance());
return ret;
}
/// <summary> Constructor for empty collections. </summary>
public static ModCollection CreateEmpty(string name, int index, int modCount)
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(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(),
new List<ModCollection>(),
new Dictionary<string, ModSettings.SavedSettings>());
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(string name, int index, int changeCounter, int version, List<ModSettings?> appliedSettings,
List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
{
Name = name;
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

@ -2,7 +2,7 @@ using Newtonsoft.Json.Linq;
using Penumbra.Services;
using Newtonsoft.Json;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Mods.Settings;
namespace Penumbra.Collections;
@ -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,21 +28,23 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
j.WriteStartObject();
j.WritePropertyName("Version");
j.WriteValue(ModCollection.CurrentVersion);
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)
@ -55,20 +57,20 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
// Inherit by collection name.
j.WritePropertyName("Inheritance");
x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Name));
x.Serialize(j, modCollection.Inheritance.Identifiers);
j.WriteEndObject();
}
public static bool LoadFromFile(FileInfo file, out string name, out int version, out Dictionary<string, ModSettings.SavedSettings> settings,
public static bool LoadFromFile(FileInfo file, out Guid id, out string name, out int version, out Dictionary<string, ModSettings.SavedSettings> settings,
out IReadOnlyList<string> inheritance)
{
settings = new Dictionary<string, ModSettings.SavedSettings>();
inheritance = Array.Empty<string>();
settings = [];
inheritance = [];
if (!file.Exists)
{
Penumbra.Log.Error("Could not read collection because file does not exist.");
name = string.Empty;
name = string.Empty;
id = Guid.Empty;
version = 0;
return false;
}
@ -76,10 +78,11 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
try
{
var obj = JObject.Parse(File.ReadAllText(file.FullName));
name = obj[nameof(ModCollection.Name)]?.ToObject<string>() ?? string.Empty;
version = obj["Version"]?.ToObject<int>() ?? 0;
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;
}
@ -87,6 +90,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
{
name = string.Empty;
version = 0;
id = Guid.Empty;
Penumbra.Log.Error($"Could not read collection information from file:\n{e}");
return false;
}

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,8 +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;
@ -10,12 +12,12 @@ using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Knowledge;
namespace Penumbra;
public class CommandHandler : IDisposable
public class CommandHandler : IDisposable, IApiService
{
private const string CommandName = "/penumbra";
@ -29,11 +31,12 @@ public class CommandHandler : IDisposable
private readonly CollectionManager _collectionManager;
private readonly Penumbra _penumbra;
private readonly CollectionEditor _collectionEditor;
private readonly KnowledgeWindow _knowledgeWindow;
public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService,
Configuration config,
ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra,
CollectionEditor collectionEditor)
Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors,
Penumbra penumbra,
CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow)
{
_commandManager = commandManager;
_redrawService = redrawService;
@ -45,6 +48,7 @@ public class CommandHandler : IDisposable
_chat = chat;
_penumbra = penumbra;
_collectionEditor = collectionEditor;
_knowledgeWindow = knowledgeWindow;
framework.RunOnFrameworkThread(() =>
{
if (_commandManager.Commands.ContainsKey(CommandName))
@ -69,21 +73,23 @@ public class CommandHandler : IDisposable
var argumentList = arguments.Split(' ', 2);
arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty;
var _ = argumentList[0].ToLowerInvariant() switch
_ = 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),
_ => 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]),
};
}
@ -121,6 +127,21 @@ public class CommandHandler : IDisposable
_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;
}
@ -304,7 +325,7 @@ public class CommandHandler : IDisposable
identifiers = _actors.FromUserString(split[2], false);
}
}
catch (ActorManager.IdentifierParseError e)
catch (ActorIdentifierFactory.IdentifierParseError e)
{
_chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true)
.AddText($" could not be converted to an identifier. {e.Message}")
@ -321,7 +342,7 @@ public class CommandHandler : IDisposable
{
_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;
}
@ -358,13 +379,13 @@ public class CommandHandler : IDisposable
}
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;
@ -375,16 +396,18 @@ public class CommandHandler : IDisposable
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)
{
@ -402,6 +425,24 @@ public class CommandHandler : IDisposable
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.")
@ -409,12 +450,35 @@ public class CommandHandler : IDisposable
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;
}
@ -496,7 +560,7 @@ public class CommandHandler : IDisposable
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;
@ -511,9 +575,9 @@ public class CommandHandler : IDisposable
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.ByName(lowerName, out var c)
: _collectionManager.Storage.ByIdentifier(lowerName, out var c)
? c
: null;
if (collection != null)
@ -552,12 +616,14 @@ public class CommandHandler : IDisposable
"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:
@ -565,7 +631,7 @@ public class CommandHandler : IDisposable
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;
@ -574,7 +640,7 @@ public class CommandHandler : IDisposable
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;
@ -585,7 +651,7 @@ public class CommandHandler : IDisposable
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;
@ -594,7 +660,7 @@ public class CommandHandler : IDisposable
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;
}
@ -619,4 +685,10 @@ public class CommandHandler : IDisposable
if (_config.PrintSuccessfulCommandsToChat)
_chat.Print(text());
}
private bool HandleKnowledge(string arguments)
{
_knowledgeWindow.Toggle();
return true;
}
}

View file

@ -1,5 +1,7 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.GameData.Data;
namespace Penumbra.Communication;
@ -10,11 +12,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the clicked object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemClick() : EventWrapper<MouseButton, object?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData, ChangedItemClick.Priority>(nameof(ChangedItemClick))
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.ChangedItemClicked"/>
/// <seealso cref="UiApi.OnChangedItemClick"/>
Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>

View file

@ -1,4 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.GameData.Data;
namespace Penumbra.Communication;
@ -8,11 +10,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.ChangedItemTooltip"/>
/// <seealso cref="UiApi.OnChangedItemHover"/>
Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>

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

@ -46,5 +46,8 @@ public sealed class CollectionChange()
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnCollectionChange"/>
ModSelection = 10,
}
}

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