Compare commits

...

166 commits

Author SHA1 Message Date
Karou
ccb5b01290 Api version bump and remove redundant framework thread call
Some checks failed
.NET Build / build (push) Has been cancelled
2025-12-05 13:39:19 +01:00
Actions User
5dd74297c6 [CI] Updating repo.json for 1.5.1.8
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-28 22:10:17 +00:00
Karou
ce54aa5d25 Added IPC call to allow for redrawing only members of specified collections
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-03 15:15:40 +01:00
Actions User
c4b6e4e00b [CI] Updating repo.json for testing_1.5.1.7
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-23 21:50:20 +00:00
Ottermandias
912c183fc6 Improve file watcher. 2025-10-23 23:45:20 +02:00
Ottermandias
5bf901d0c4 Update actorobjectmanager when setting cutscene index.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-23 17:30:29 +02:00
Ottermandias
cbedc878b9 Slight cleanup and autoformat. 2025-10-22 21:56:16 +02:00
Ottermandias
c8cf560fc1 Merge branch 'refs/heads/StoiaCode/fileWatcher' 2025-10-22 21:48:42 +02:00
Stoia
f05cb52da2 Add Option to notify instead of auto install.
And General Fixes
2025-10-22 18:20:44 +02:00
Ottermandias
7ed81a9823 Update OtterGui.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-22 17:53:02 +02:00
Stoia
60aa23efcd
Merge branch 'xivdev:master' into fileWatcher 2025-10-22 14:28:08 +02:00
Ottermandias
ebbe957c95 Remove login screen log spam.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-11 20:13:51 +02:00
Actions User
300e0e6d84 [CI] Updating repo.json for 1.5.1.6
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-07 10:45:04 +00:00
Ottermandias
049baa4fe4 Again. 2025-10-07 12:42:54 +02:00
Ottermandias
0881dfde8a Update signatures. 2025-10-07 12:27:35 +02:00
Actions User
23c0506cb8 [CI] Updating repo.json for testing_1.5.1.5
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-28 10:43:01 +00:00
Ottermandias
699745413e Make priority an int. 2025-09-28 12:40:52 +02:00
Actions User
eb53f04c6b [CI] Updating repo.json for testing_1.5.1.4
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 12:03:35 +00:00
Ottermandias
c6b596169c Add default constructor. 2025-09-27 14:01:21 +02:00
Actions User
a0c3e820b0 [CI] Updating repo.json for testing_1.5.1.3
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 11:02:39 +00:00
Ottermandias
a59689ebfe CS API update and add http API routes. 2025-09-27 13:00:18 +02:00
Exter-N
e9f67a009b Lift "shaders known" restriction for saving materials
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-19 11:18:39 +02:00
Ottermandias
97c8d82b33 Prevent default-named collection from being renamed and always put it at the top of the selector.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-07 10:45:28 +02:00
Stoia
c3b00ff426 Integrate FileWatcher
HEAVY WIP
2025-09-06 14:22:18 +02:00
Actions User
6348c4a639 [CI] Updating repo.json for 1.5.1.2
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-02 14:25:55 +00:00
Ottermandias
5a6e06df3b git is stupid 2025-09-02 16:22:02 +02:00
Ottermandias
f5f6dd3246 Handle some TODOs. 2025-09-02 16:12:01 +02:00
Ottermandias
4e788f7c2b Update sig. 2025-09-02 11:51:59 +02:00
Ottermandias
ad1659caf6 Update libraries. 2025-09-02 11:29:58 +02:00
Ottermandias
18a6ce2a5f Merge branch 'refs/heads/Exter-N/cldapi'
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-01 15:59:26 +02:00
Ottermandias
e68e821b2a Merge branch 'master' into Exter-N/cldapi 2025-09-01 15:58:22 +02:00
Ottermandias
96764b34ca Merge branch 'refs/heads/Exter-N/restree-stuff' 2025-09-01 15:57:06 +02:00
Exter-N
2cf60b78cd Reject and warn about cloud-synced base directories 2025-08-31 06:42:45 +02:00
Exter-N
d59be1e660 Refine IsCloudSynced 2025-08-31 05:25:37 +02:00
Exter-N
5503bb32e0 CloudApi testing in Debug tab 2025-08-31 04:13:56 +02:00
Exter-N
f3ec4b2e08 Only display the file name and last dir for externals 2025-08-30 19:19:07 +02:00
Exter-N
b3379a9710 Stop redacting external paths 2025-08-30 16:55:20 +02:00
Exter-N
8c25ef4b47 Make the save button ResourceTreeViewer baseline 2025-08-30 16:53:12 +02:00
Ottermandias
912020cc3f Update for staging and wrong tooltip.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-29 16:36:42 +02:00
Ottermandias
be8987a451 Merge branch 'master' of github.com:xivDev/Penumbra
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-28 18:52:29 +02:00
Ottermandias
f7cf5503bb Fix deleting PCP collections. 2025-08-28 18:52:06 +02:00
Ottermandias
a04a5a071c Add warning in file redirections if extension doesn't match. 2025-08-28 18:51:57 +02:00
Actions User
71e24c13c7 [CI] Updating repo.json for 1.5.1.0
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-25 08:39:42 +00:00
Ottermandias
c0120f81af 1.5.1.0 2025-08-25 10:37:38 +02:00
Ottermandias
da47c19aeb Woops, increment version. 2025-08-25 10:25:05 +02:00
Actions User
e16800f216 [CI] Updating repo.json for testing_1.5.0.10 2025-08-25 08:16:04 +00:00
Ottermandias
79a4fc5904 Fix wrong logging. 2025-08-25 10:13:48 +02:00
Ottermandias
bf90725dd2 Fix resolvecontext issue. 2025-08-25 10:13:39 +02:00
Ottermandias
a14347f73a Update temporary collection creation. 2025-08-25 10:13:31 +02:00
Actions User
1e07e43498 [CI] Updating repo.json for testing_1.5.0.9
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-24 13:51:43 +00:00
Ottermandias
f51f8a7bf8 Try to filter meta entries for relevance. 2025-08-24 15:24:57 +02:00
Exter-N
1fca78fa71 Add Kdb files to ResourceTree 2025-08-24 14:09:02 +02:00
Exter-N
c8b6325a87 Add game integrity message to On-Screen 2025-08-24 14:06:39 +02:00
Ottermandias
6079103505 Add collection PCP settings.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-23 14:46:27 +02:00
Actions User
d302a17f1f [CI] Updating repo.json for testing_1.5.0.8
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-22 18:33:43 +00:00
Ottermandias
0d64384059 Add cleanup buttons to PCP, add option to turn off PCP IPC. 2025-08-22 20:31:40 +02:00
Ottermandias
10894d451a Add Pcp Events. 2025-08-22 18:08:22 +02:00
Actions User
fb34238530 [CI] Updating repo.json for testing_1.5.0.7 2025-08-22 13:51:50 +00:00
Ottermandias
8043e6fb6b Add option to disable PCP. 2025-08-22 15:49:15 +02:00
Ottermandias
e3b7f72893 Add initial PCP. 2025-08-22 15:44:33 +02:00
Ottermandias
b7f326e29c Fix bug with collection setting and empty collection. 2025-08-22 15:43:55 +02:00
Ottermandias
dad01e1af8 Update GameData. 2025-08-20 15:24:00 +02:00
Ottermandias
10b71930a1 Merge branch 'refs/heads/Exter-N/stockings-skin-slot' 2025-08-18 15:41:22 +02:00
Ottermandias
23257f94a4 Some cleanup and add option to disable skin material attribute scanning. 2025-08-18 15:41:10 +02:00
Ottermandias
83a36ed4cb Merge branch 'master' into Exter-N/stockings-skin-slot 2025-08-18 15:31:52 +02:00
Ottermandias
8304579d29 Add predefined tags to the multi mod selector. 2025-08-17 13:54:36 +02:00
Actions User
24cbc6c5e1 [CI] Updating repo.json for 1.5.0.6 2025-08-17 08:46:26 +00:00
Exter-N
41edc23820 Allow changing the skin mtrl suffix 2025-08-17 03:11:11 +02:00
Exter-N
aa920b5e9b Fix ImGui texture usage issue 2025-08-17 01:41:49 +02:00
Ottermandias
87ace28bcf Update OtterGui. 2025-08-16 11:56:24 +02:00
Ottermandias
5917f5fad1 Small fixes. 2025-08-13 17:42:45 +02:00
Actions User
f69c264317 [CI] Updating repo.json for 1.5.0.5 2025-08-13 14:53:11 +00:00
Ottermandias
a7246b9d98 Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it. 2025-08-13 16:50:26 +02:00
Actions User
9aff388e21 [CI] Updating repo.json for 1.5.0.4 2025-08-12 12:53:33 +00:00
Ottermandias
091aff1b8a Merge branch 'master' of github.com:xivDev/Penumbra 2025-08-12 14:47:50 +02:00
Ottermandias
9f8185f67b Add new parameter to LoadWeapon hook. 2025-08-12 14:47:35 +02:00
Actions User
b112d75a27 [CI] Updating repo.json for 1.5.0.3 2025-08-12 10:31:13 +00:00
Ottermandias
7af81a6c18 Fix issue with removing default metadata. 2025-08-12 12:29:09 +02:00
Ottermandias
12a218bb2b Protect against empty requested paths. 2025-08-12 12:28:56 +02:00
Ottermandias
f6bac93db7 Update ChangedEquipData. 2025-08-11 19:58:24 +02:00
Actions User
155d3d49aa [CI] Updating repo.json for 1.5.0.2 2025-08-09 16:40:42 +00:00
Exter-N
9aae2210a2 Fix nullptr crashes 2025-08-09 14:57:15 +02:00
Actions User
3785a629ce [CI] Updating repo.json for 1.5.0.1 2025-08-09 11:03:24 +00:00
Ottermandias
02af52671f Need staging again ... 2025-08-09 13:00:40 +02:00
Ottermandias
391c9d727e Fix shifted timeline vfunc offset. 2025-08-09 12:51:39 +02:00
Ottermandias
ff2b2be953 Fix popups not working early. 2025-08-09 12:11:29 +02:00
Ottermandias
6242b30f93 Fix resizable child. 2025-08-09 11:58:35 +02:00
Exter-N
11cd08a9de ClientStructs-ify stuff 2025-08-09 10:29:29 +02:00
Ottermandias
46cfbcb115 Set Repo API level to 13 and remove stg from future releases. 2025-08-08 23:13:23 +02:00
Actions User
66543cc671 [CI] Updating repo.json for 1.5.0.0 2025-08-08 21:12:00 +00:00
Ottermandias
13283c9690 Fix dumb. 2025-08-08 23:08:26 +02:00
Ottermandias
bedfb22466 Use staging for release. 2025-08-08 23:04:50 +02:00
Ottermandias
13df8b2248 Update gamedata. 2025-08-08 23:02:22 +02:00
Ottermandias
93406e4d4e 1.5.0.0 2025-08-08 16:17:59 +02:00
Ridan Vandenbergh
8140d08557 Add vertex material types for usages of 2 colour attributes 2025-08-08 16:15:19 +02:00
Ridan Vandenbergh
2b36f39848 Fix basecolor texture in material export 2025-08-08 16:12:37 +02:00
Ottermandias
a69811800d Update GameData 2025-08-08 15:56:25 +02:00
Ottermandias
3f18ad50de Initial API13 / 7.3 update. 2025-08-08 00:45:24 +02:00
Ridan Vandenbergh
6689e326ee Material tab: disallow "Enable Transparency" for stockings shader 2025-08-02 00:38:24 +02:00
Passive
bdcab22a55 Cleanup methods to extension class 2025-08-02 00:16:55 +02:00
Passive
f5f4fe7259 Invalid tangent fix example 2025-08-02 00:16:55 +02:00
Sebastina
898963fea5 Allow focusing a specified mod via HTTP API under the mods tab. 2025-08-02 00:16:38 +02:00
Ottermandias
8527bfa29c Fix missing updates for OtterGui. 2025-08-02 00:13:35 +02:00
Ottermandias
baca3cdec2 Update Libs. 2025-08-02 00:08:09 +02:00
Ottermandias
dc93eba34c Add initial complex group things. 2025-08-02 00:06:25 +02:00
Ottermandias
012052daa0 Change behavior for directory names. 2025-08-02 00:06:03 +02:00
Ottermandias
a9546e31ee Update packages. 2025-08-02 00:05:27 +02:00
Actions User
a4a6283e7b [CI] Updating repo.json for testing_1.4.0.6 2025-07-14 15:12:06 +00:00
Ottermandias
00c02fd16e Fix tex file migration for small textures. 2025-07-14 17:09:07 +02:00
Ottermandias
140d150bb4 Fix character sound data. 2025-07-14 17:08:46 +02:00
Actions User
49a6d935f3 [CI] Updating repo.json for testing_1.4.0.5 2025-07-05 20:11:28 +00:00
Ottermandias
692beacc2e Merge remote-tracking branch 'Exter-N/human-skin-materials' 2025-07-05 22:04:48 +02:00
Ottermandias
a953febfba Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models. 2025-07-05 22:03:32 +02:00
Ottermandias
c0aa2e36ea Merge branch 'refs/heads/Exter-N/reslogger-tid' 2025-07-05 22:03:14 +02:00
Exter-N
278bf43b29 ClientStructs-ify ResourceTree stuff 2025-07-05 05:20:24 +02:00
Exter-N
a97d9e4953 Add Human skin material handling 2025-07-05 04:37:37 +02:00
Exter-N
30e3cd1f38 Add OS thread ID info to the Resource Logger 2025-07-04 19:41:31 +02:00
Ottermandias
62e9dc164d Add support button. 2025-06-26 14:49:28 +02:00
Actions User
9fc572ba0c [CI] Updating repo.json for testing_1.4.0.4 2025-06-15 21:47:41 +00:00
Ottermandias
3c20b541ce Make mousewheel-scrolling work for setting combos, also filters. 2025-06-15 23:20:13 +02:00
Ottermandias
1961b03d37 Fix issues with shapes and attributes with ID. 2025-06-15 23:18:46 +02:00
Ottermandias
1f4ec984b3 Use improved filesystem. 2025-06-13 17:27:56 +02:00
Ottermandias
4981b0348f BNPCs. 2025-06-13 17:27:56 +02:00
Ottermandias
a8c05fc6ee Make middle-mouse button handle temporary settings. 2025-06-13 17:27:56 +02:00
Actions User
3d05662384 [CI] Updating repo.json for testing_1.4.0.3 2025-06-08 09:38:30 +00:00
Ottermandias
973814b31b Some more BNPCs. 2025-06-08 11:36:32 +02:00
Ottermandias
a16fd85a7e Handle .tex files with broken mip map offsets on import, also remove unnecessary mipmaps (any after reaching minimum size once). 2025-06-08 11:28:12 +02:00
Ottermandias
4c0e6d2a67 Update Mod Merger for other group types. 2025-06-07 22:10:59 +02:00
Ottermandias
535694e9c8 Update some BNPC Names. 2025-06-07 22:10:17 +02:00
Ottermandias
318a41fe52 Add checking for supported features with the currently new supported features 'Atch', 'Shp' and 'Atr'. 2025-06-03 18:39:54 +02:00
Actions User
98203e4e8a [CI] Updating repo.json for testing_1.4.0.2 2025-06-01 11:06:37 +00:00
Ottermandias
6cba63ac98 Make shape names editable in models. 2025-06-01 13:04:26 +02:00
Ottermandias
b48c4f440a Make attributes and shapes completely toggleable. 2025-06-01 13:04:26 +02:00
Actions User
75f4e66dbf [CI] Updating repo.json for 1.4.0.1 2025-05-30 12:38:32 +00:00
Ottermandias
74bd1cf911 Fix checking the flags for all races and genders for specific IDs in shapes/attributes. 2025-05-30 14:36:33 +02:00
Ottermandias
ff2a9f95c4 Fix Atr and Shp not being transmitted via Mare, add improved compression but don't use it yet. 2025-05-30 14:36:07 +02:00
Ottermandias
9921c3332e Merge branch 'master' of github.com:xivDev/Penumbra 2025-05-30 14:35:24 +02:00
Ottermandias
f2927290f5 Fix exceptions when unsubscribing during event invocation. 2025-05-30 14:35:13 +02:00
Actions User
1551d9b6f3 [CI] Updating repo.json for 1.4.0.0 2025-05-28 11:56:49 +00:00
Ottermandias
5e985f4a84 1.4.0.0 2025-05-28 13:54:42 +02:00
Ottermandias
2c115eda94 Slightly improve error message when importing wrongly named atch files. 2025-05-28 13:54:23 +02:00
Ottermandias
ebe45c6a47 Update Lib. 2025-05-27 11:33:10 +02:00
Ottermandias
82fc334be7 Use dynamis for some pointers. 2025-05-23 15:17:19 +02:00
Actions User
cd56163b1b [CI] Updating repo.json for testing_1.3.6.15 2025-05-23 09:32:11 +00:00
Ottermandias
ccc2c1fd4c Fix missing other option notifications for shp/atr. 2025-05-23 11:30:10 +02:00
Ottermandias
08c9124858 Fix issue with shapes/attributes not checking the groups correctly. 2025-05-23 11:29:52 +02:00
Ottermandias
1bdbfe22c1 Update Libraries. 2025-05-23 10:50:04 +02:00
Actions User
9e7c304556 [CI] Updating repo.json for testing_1.3.6.14 2025-05-22 09:16:22 +00:00
Ottermandias
bc4f88aee9 Fix shape/attribute mask stupidity. 2025-05-22 11:14:17 +02:00
Ottermandias
400d7d0bea Slight improvements. 2025-05-22 11:13:58 +02:00
Ottermandias
ac4c75d3c3 Fix not updating meta count correctly. 2025-05-22 11:13:42 +02:00
Ottermandias
507b0a5aee Slight description update. 2025-05-21 18:07:17 +02:00
Actions User
f5db888bbd [CI] Updating repo.json for testing_1.3.6.13 2025-05-21 13:49:29 +00:00
Ottermandias
d7dee39fab Add attribute handling, rework atr and shape caches. 2025-05-21 15:45:05 +02:00
Ottermandias
3412786282 Optimize used memory by metadictionarys a bit. 2025-05-21 15:45:05 +02:00
Ottermandias
861cbc7759 Add global EQP edits to always hide horns or ears. 2025-05-21 15:45:05 +02:00
Actions User
fefa3852f7 [CI] Updating repo.json for testing_1.3.6.12 2025-05-19 15:17:54 +00:00
Ottermandias
68b68d6ce7 Fix some issues with customization IDs and supported counts. 2025-05-19 17:15:29 +02:00
Ottermandias
47b5895404 Fix issue with temp settings again. 2025-05-18 22:00:17 +02:00
Actions User
e18e4bb0e1 [CI] Updating repo.json for testing_1.3.6.11 2025-05-18 14:02:16 +00:00
Ottermandias
6e4e28fa00 Fix disabling conditional shapes. 2025-05-18 16:00:09 +02:00
Ottermandias
e326e3d809 Update shp conditions. 2025-05-18 15:52:47 +02:00
Ottermandias
fbc4c2d054 Improve option select combo. 2025-05-18 12:54:23 +02:00
Ottermandias
3078c467d0 Fix issue with empty and temporary settings. 2025-05-18 12:54:23 +02:00
Ottermandias
52927ff06b Fix clipping in meta edits. 2025-05-18 12:54:23 +02:00
Actions User
08e8b9d2a4 [CI] Updating repo.json for testing_1.3.6.10 2025-05-15 22:28:36 +00:00
220 changed files with 6054 additions and 1373 deletions

View file

@ -20,7 +20,7 @@ jobs:
run: dotnet restore run: dotnet restore
- name: Download Dalamud - name: Download Dalamud
run: | 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" Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build - name: Build
run: | run: |

@ -1 +1 @@
Subproject commit f130c928928cb0d48d3c807b7df5874c2460fe98 Subproject commit a63f6735cf4bed4f7502a022a10378607082b770

@ -1 +1 @@
Subproject commit 1465203967d08519c6716292bc5e5094c7fbcacc Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf

View file

@ -1,4 +1,4 @@
<Project Sdk="Dalamud.NET.Sdk/12.0.2"> <Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
</PropertyGroup> </PropertyGroup>

@ -1 +1 @@
Subproject commit 8e57c2e12570bb1795efb9e5c6e38617aa8dd5e3 Subproject commit d889f9ef918514a46049725052d378b441915b00

@ -1 +1 @@
Subproject commit 0e5dcd1a5687ec5f8fa2ef2526b94b9a0ea1b5b5 Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793

View file

@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
schemas\structs\group_single.json = schemas\structs\group_single.json schemas\structs\group_single.json = schemas\structs\group_single.json
schemas\structs\manipulation.json = schemas\structs\manipulation.json schemas\structs\manipulation.json = schemas\structs\manipulation.json
schemas\structs\meta_atch.json = schemas\structs\meta_atch.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_enums.json = schemas\structs\meta_enums.json
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json

View file

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

View file

@ -66,6 +66,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(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.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.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); return Functions.ToCompressedBase64(array, 0);
@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
} }
WriteCache(zipStream, cache.Atch); WriteCache(zipStream, cache.Atch);
WriteCache(zipStream, cache.Shp);
WriteCache(zipStream, cache.Atr);
} }
} }
@ -140,6 +144,86 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
} }
} }
public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8);
public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8);
public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P';
public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8);
public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8);
public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8);
public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P';
public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H';
public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8);
public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8);
private static unsafe string CompressMetaManipulationsV2(ModCollection? collection)
{
using var ms = new MemoryStream();
ms.Capacity = 1024;
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
{
zipStream.Write((byte)2);
zipStream.Write("META0002"u8);
if (collection?.MetaCache is { } cache)
{
WriteCache(zipStream, cache.Imc, ImcKey);
WriteCache(zipStream, cache.Eqp, EqpKey);
WriteCache(zipStream, cache.Eqdp, EqdpKey);
WriteCache(zipStream, cache.Est, EstKey);
WriteCache(zipStream, cache.Rsp, RspKey);
WriteCache(zipStream, cache.Gmp, GmpKey);
cache.GlobalEqp.EnterReadLock();
try
{
if (cache.GlobalEqp.Count > 0)
{
zipStream.Write(GeqpKey);
zipStream.Write(cache.GlobalEqp.Count);
foreach (var (globalEqp, _) in cache.GlobalEqp)
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
}
}
finally
{
cache.GlobalEqp.ExitReadLock();
}
WriteCache(zipStream, cache.Atch, AtchKey);
WriteCache(zipStream, cache.Shp, ShpKey);
WriteCache(zipStream, cache.Atr, AtrKey);
}
}
ms.Flush();
ms.Position = 0;
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
return Convert.ToBase64String(data);
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache, uint label)
where TKey : unmanaged, IMetaIdentifier
where TValue : unmanaged
{
metaCache.EnterReadLock();
try
{
if (metaCache.Count <= 0)
return;
stream.Write(label);
stream.Write(metaCache.Count);
foreach (var (identifier, (_, value)) in metaCache)
{
stream.Write(identifier);
stream.Write(value);
}
}
finally
{
metaCache.ExitReadLock();
}
}
}
/// <summary> /// <summary>
/// Convert manipulations from a transmitted base64 string to actual manipulations. /// Convert manipulations from a transmitted base64 string to actual manipulations.
/// The empty string is treated as an empty set. /// The empty string is treated as an empty set.
@ -170,6 +254,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
{ {
case 0: return ConvertManipsV0(data, out manips); case 0: return ConvertManipsV0(data, out manips);
case 1: return ConvertManipsV1(data, out manips); case 1: return ConvertManipsV1(data, out manips);
case 2: return ConvertManipsV2(data, out manips);
default: default:
Penumbra.Log.Debug($"Invalid version for manipulations: {version}."); Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
manips = null; manips = null;
@ -185,6 +270,131 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
} }
} }
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) private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{ {
if (!data.StartsWith("META0001"u8)) if (!data.StartsWith("META0001"u8))
@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
if (!identifier.Validate() || !manips.TryAdd(identifier, value)) if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false; 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; return true;

View file

@ -1,3 +1,4 @@
using Newtonsoft.Json.Linq;
using OtterGui.Compression; using OtterGui.Compression;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
@ -33,12 +34,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
{ {
switch (type) switch (type)
{ {
case ModPathChangeType.Deleted when oldDirectory != null: case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break;
ModDeleted?.Invoke(oldDirectory.Name); case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break;
break;
case ModPathChangeType.Added when newDirectory != null:
ModAdded?.Invoke(newDirectory.Name);
break;
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
break; break;
@ -46,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
} }
public void Dispose() public void Dispose()
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); {
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
}
public Dictionary<string, string> GetModList() public Dictionary<string, string> GetModList()
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text); => _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
@ -109,10 +108,22 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
public event Action<string>? ModAdded; public event Action<string>? ModAdded;
public event Action<string, string>? ModMoved; 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) public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
{ {
if (!_modManager.TryGetMod(modDirectory, modName, out var mod) if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.FindLeaf(mod, out var leaf)) || !_modFileSystem.TryGetValue(mod, out var leaf))
return (PenumbraApiEc.ModMissing, string.Empty, false, false); return (PenumbraApiEc.ModMissing, string.Empty, false, false);
var fullPath = leaf.FullName(); var fullPath = leaf.FullName();
@ -127,7 +138,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod) if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.FindLeaf(mod, out var leaf)) || !_modFileSystem.TryGetValue(mod, out var leaf))
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
try try

View file

@ -17,7 +17,7 @@ public class PenumbraApi(
UiApi ui) : IDisposable, IApiService, IPenumbraApi UiApi ui) : IDisposable, IApiService, IPenumbraApi
{ {
public const int BreakingVersion = 5; public const int BreakingVersion = 5;
public const int FeatureVersion = 9; public const int FeatureVersion = 13;
public void Dispose() public void Dispose()
{ {

View file

@ -1,39 +1,38 @@
using System.Collections.Frozen;
using Newtonsoft.Json; using Newtonsoft.Json;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Api.Api; namespace Penumbra.Api.Api;
public class PluginStateApi : IPenumbraApiPluginState, IApiService public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService
{ {
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public PluginStateApi(Configuration config, CommunicatorService communicator)
{
_config = config;
_communicator = communicator;
}
public string GetModDirectory() public string GetModDirectory()
=> _config.ModDirectory; => config.ModDirectory;
public string GetConfiguration() public string GetConfiguration()
=> JsonConvert.SerializeObject(_config, Formatting.Indented); => JsonConvert.SerializeObject(config, Formatting.Indented);
public event Action<string, bool>? ModDirectoryChanged public event Action<string, bool>? ModDirectoryChanged
{ {
add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
remove => _communicator.ModDirectoryChanged.Unsubscribe(value!); remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
} }
public bool GetEnabledState() public bool GetEnabledState()
=> _config.EnableMods; => config.EnableMods;
public event Action<bool>? EnabledChange public event Action<bool>? EnabledChange
{ {
add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
remove => _communicator.EnabledChanged.Unsubscribe(value!); 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

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

View file

@ -20,8 +20,16 @@ public class TemporaryApi(
ApiHelpers apiHelpers, ApiHelpers apiHelpers,
ModManager modManager) : IPenumbraApiTemporary, IApiService ModManager modManager) : IPenumbraApiTemporary, IApiService
{ {
public Guid CreateTemporaryCollection(string name) public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
=> tempCollections.CreateTemporaryCollection(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) public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
=> tempCollections.RemoveTemporaryCollection(collectionId) => tempCollections.RemoveTemporaryCollection(collectionId)

View file

@ -5,6 +5,7 @@ using EmbedIO.WebApi;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Api; using Penumbra.Api.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Mods.Settings;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -13,12 +14,15 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller : WebApiController private partial class Controller : WebApiController
{ {
// @formatter:off // @formatter:off
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); [Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll(); [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); [Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod(); [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow(); [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 // @formatter:on
} }
@ -64,6 +68,12 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller(IPenumbraApi api, IFramework framework) private partial class Controller(IPenumbraApi api, IFramework framework)
{ {
public partial string GetModDirectory()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
return api.PluginState.GetModDirectory();
}
public partial object? GetMods() public partial object? GetMods()
{ {
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
@ -116,6 +126,38 @@ public class HttpApi : IDisposable, IApiService
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
} }
public async partial Task FocusMod()
{
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered.");
if (data.Path.Length != 0)
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
}
public async partial Task SetModSettings()
{
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
await framework.RunOnFrameworkThread(() =>
{
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
if (data.Inherit.HasValue)
{
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
if (data.Inherit.Value)
return;
}
if (data.State.HasValue)
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
if (data.Priority.HasValue)
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
foreach (var (group, settings) in data.Settings ?? [])
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
}
).ConfigureAwait(false);
}
private record ModReloadData(string Path, string Name) private record ModReloadData(string Path, string Name)
{ {
public ModReloadData() public ModReloadData()
@ -123,6 +165,13 @@ public class HttpApi : IDisposable, IApiService
{ } { }
} }
private record ModFocusData(string Path, string Name)
{
public ModFocusData()
: this(string.Empty, string.Empty)
{ }
}
private record ModInstallData(string Path) private record ModInstallData(string Path)
{ {
public ModInstallData() public ModInstallData()
@ -136,5 +185,19 @@ public class HttpApi : IDisposable, IApiService
: this(string.Empty, RedrawType.Redraw, -1) : 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

@ -54,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ModDeleted.Provider(pi, api.Mods), IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
IpcSubscribers.ModAdded.Provider(pi, api.Mods), IpcSubscribers.ModAdded.Provider(pi, api.Mods),
IpcSubscribers.ModMoved.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.GetModPath.Provider(pi, api.Mods),
IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
@ -80,10 +82,13 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState), IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
IpcSubscribers.EnabledChange.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.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw), IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve), IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve), IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),

View file

@ -1,7 +1,7 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
@ -121,6 +121,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
}).ToArray(); }).ToArray();
ImGui.OpenPopup("Changed Item List"); 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() private void DrawChangedItemPopup()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Extensions; using OtterGui.Extensions;
using OtterGui.Raii; using OtterGui.Raii;

View file

@ -1,6 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Extensions; using OtterGui.Extensions;
using OtterGui.Raii; using OtterGui.Raii;
@ -38,6 +38,7 @@ public class TemporaryIpcTester(
private string _tempGamePath = "test/game/path.mtrl"; private string _tempGamePath = "test/game/path.mtrl";
private string _tempFilePath = "test/success.mtrl"; private string _tempFilePath = "test/success.mtrl";
private string _tempManipulation = string.Empty; private string _tempManipulation = string.Empty;
private string _identity = string.Empty;
private PenumbraApiEc _lastTempError; private PenumbraApiEc _lastTempError;
private int _tempActorIndex; private int _tempActorIndex;
private bool _forceOverwrite; private bool _forceOverwrite;
@ -48,6 +49,7 @@ public class TemporaryIpcTester(
if (!_) if (!_)
return; return;
ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128);
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
@ -73,7 +75,7 @@ public class TemporaryIpcTester(
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection"); IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
if (ImGui.Button("Create##Collection")) if (ImGui.Button("Create##Collection"))
{ {
LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName); _lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
if (_tempGuid == null) if (_tempGuid == null)
{ {
_tempGuid = LastCreatedCollectionId; _tempGuid = LastCreatedCollectionId;
@ -282,7 +284,7 @@ public class TemporaryIpcTester(
foreach (var mod in list) foreach (var mod in list)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Name); ImGui.TextUnformatted(mod.Name.Text);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Priority.ToString()); ImGui.TextUnformatted(mod.Priority.ToString());
ImGui.TableNextColumn(); ImGui.TableNextColumn();

View file

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

View file

@ -1,4 +1,4 @@
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui.Text; using OtterGui.Text;
namespace Penumbra; namespace Penumbra;

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

@ -247,6 +247,8 @@ public sealed class CollectionCache : IDisposable
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Shp) foreach (var (identifier, entry) in files.Manipulations.Shp)
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Atr)
AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp) foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!); AddManipulation(mod, identifier, null!);
} }

View file

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

View file

@ -17,11 +17,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
public readonly ImcCache Imc = new(manager, collection); public readonly ImcCache Imc = new(manager, collection);
public readonly AtchCache Atch = new(manager, collection); public readonly AtchCache Atch = new(manager, collection);
public readonly ShpCache Shp = new(manager, collection); public readonly ShpCache Shp = new(manager, collection);
public readonly AtrCache Atr = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new(); public readonly GlobalEqpCache GlobalEqp = new();
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
public int Count public int Count
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count; => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
@ -32,6 +33,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
.Concat(Imc.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(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Shp.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))); .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void Reset() public void Reset()
@ -44,6 +46,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Imc.Reset(); Imc.Reset();
Atch.Reset(); Atch.Reset();
Shp.Reset(); Shp.Reset();
Atr.Reset();
GlobalEqp.Clear(); GlobalEqp.Clear();
} }
@ -61,6 +64,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Imc.Dispose(); Imc.Dispose();
Atch.Dispose(); Atch.Dispose();
Shp.Dispose(); Shp.Dispose();
Atr.Dispose();
} }
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
@ -76,6 +80,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
RspIdentifier i => Rsp.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), AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
ShpIdentifier i => Shp.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), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
_ => false, _ => false,
}; };
@ -98,6 +103,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
RspIdentifier i => Rsp.RevertMod(i, out mod), RspIdentifier i => Rsp.RevertMod(i, out mod),
AtchIdentifier i => Atch.RevertMod(i, out mod), AtchIdentifier i => Atch.RevertMod(i, out mod),
ShpIdentifier i => Shp.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), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
_ => (mod = null) != null, _ => (mod = null) != null,
}; };
@ -115,6 +121,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
RspIdentifier i when entry is RspEntry e => Rsp.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), AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
ShpIdentifier i when entry is ShpEntry e => Shp.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), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
_ => false, _ => false,
}; };

View file

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

View file

@ -7,162 +7,100 @@ namespace Penumbra.Collections.Cache;
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection) public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection)
{ {
public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
=> _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is true;
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State public bool ShouldBeDisabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
=> _shpData; => DisabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is false;
internal IEnumerable<(ShapeString, IReadOnlyDictionary<ShapeString, ShpHashSet>)> ConditionState internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> State(ShapeConnectorCondition connector)
=> _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary<ShapeString, ShpHashSet>)kvp.Value)); => connector switch
public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary<ShapeString, ShpHashSet>? dict)
{
if (_conditionalSet.TryGetValue(condition, out var d))
{ {
dict = d; ShapeConnectorCondition.None => _shpData,
return true; ShapeConnectorCondition.Wrists => _wristConnectors,
} ShapeConnectorCondition.Waist => _waistConnectors,
ShapeConnectorCondition.Ankles => _ankleConnectors,
_ => [],
};
dict = null; public int EnabledCount { get; private set; }
return false; public int DisabledCount { get; private set; }
}
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _shpData = [];
public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _wristConnectors = [];
{ private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _waistConnectors = [];
private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _ankleConnectors = [];
public bool All
{
get => _allIds[^1];
set => _allIds[^1] = value;
}
public bool this[HumanSlot slot]
{
get
{
if (slot is HumanSlot.Unknown)
return All;
return _allIds[(int)slot];
}
set
{
if (slot is HumanSlot.Unknown)
_allIds[^1] = value;
else
_allIds[(int)slot] = value;
}
}
public bool Contains(HumanSlot slot, PrimaryId id)
=> All || this[slot] || Contains((slot, id));
public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value)
{
if (slot is HumanSlot.Unknown)
{
var old = All;
All = value.Value;
return old != value.Value;
}
if (!id.HasValue)
{
var old = this[slot];
this[slot] = value.Value;
return old != value.Value;
}
if (value.Value)
return Add((slot, id.Value));
return Remove((slot, id.Value));
}
public new void Clear()
{
base.Clear();
_allIds.SetAll(false);
}
public bool IsEmpty
=> !_allIds.HasAnySet() && Count is 0;
}
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
private readonly Dictionary<ShapeString, Dictionary<ShapeString, ShpHashSet>> _conditionalSet = [];
public void Reset() public void Reset()
{ {
Clear(); Clear();
_shpData.Clear(); _shpData.Clear();
_conditionalSet.Clear(); _wristConnectors.Clear();
_waistConnectors.Clear();
_ankleConnectors.Clear();
EnabledCount = 0;
DisabledCount = 0;
} }
protected override void Dispose(bool _) protected override void Dispose(bool _)
=> Clear(); => Reset();
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
{ {
if (identifier.ShapeCondition.Length > 0) switch (identifier.ConnectorCondition)
{ {
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) case ShapeConnectorCondition.None: Func(_shpData); break;
{ case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
if (!entry.Value) case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
return; case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
shapes = new Dictionary<ShapeString, ShpHashSet>();
_conditionalSet.Add(identifier.ShapeCondition, shapes);
}
Func(shapes);
}
else
{
Func(_shpData);
} }
void Func(Dictionary<ShapeString, ShpHashSet> dict) return;
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
{ {
if (!dict.TryGetValue(identifier.Shape, out var value)) if (!dict.TryGetValue(identifier.Shape, out var value))
{ {
if (!entry.Value)
return;
value = []; value = [];
dict.Add(identifier.Shape, value); dict.Add(identifier.Shape, value);
} }
value.TrySet(identifier.Slot, identifier.Id, entry); if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _))
{
if (entry.Value)
++EnabledCount;
else
++DisabledCount;
}
} }
} }
protected override void RevertModInternal(ShpIdentifier identifier) protected override void RevertModInternal(ShpIdentifier identifier)
{ {
if (identifier.ShapeCondition.Length > 0) switch (identifier.ConnectorCondition)
{ {
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) case ShapeConnectorCondition.None: Func(_shpData); break;
return; case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
Func(shapes); case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
}
else
{
Func(_shpData);
} }
return; return;
void Func(Dictionary<ShapeString, ShpHashSet> dict) void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
{ {
if (!_shpData.TryGetValue(identifier.Shape, out var value)) if (!dict.TryGetValue(identifier.Shape, out var value))
return; return;
if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which))
_shpData.Remove(identifier.Shape); {
if (which)
--EnabledCount;
else
--DisabledCount;
if (value.IsEmpty)
dict.Remove(identifier.Shape);
}
} }
} }
} }

View file

@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable
return; return;
var collection = _resolver.PlayerCollection(); var collection = _resolver.PlayerCollection();
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); if (collection.Identity.Id == Guid.Empty)
_collections.SetCollection(collection, CollectionType.Current); {
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);
}
} }

View file

@ -1,7 +1,7 @@
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Api; using Penumbra.Api.Api;

View file

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

View file

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

View file

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

View file

@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra; namespace Penumbra;
public record PcpSettings
{
public bool CreateCollection { get; set; } = true;
public bool AssignCollection { get; set; } = true;
public bool AllowIpc { get; set; } = true;
public bool DisableHandling { get; set; } = false;
public string FolderName { get; set; } = "PCP";
}
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration, ISavable, IService public class Configuration : IPluginConfiguration, ISavable, IService
{ {
@ -44,6 +53,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public string ModDirectory { get; set; } = string.Empty; public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty;
public string WatchDirectory { get; set; } = string.Empty;
public bool? UseCrashHandler { get; set; } = null; public bool? UseCrashHandler { get; set; } = null;
public bool OpenWindowAtStart { get; set; } = false; public bool OpenWindowAtStart { get; set; } = false;
@ -67,10 +77,13 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool HideRedrawBar { get; set; } = false; public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public bool DefaultTemporaryMode { get; set; } = false; public bool DefaultTemporaryMode { get; set; } = false;
public bool EnableDirectoryWatch { get; set; } = false;
public bool EnableAutomaticModImport { get; set; } = false;
public bool EnableCustomShapes { get; set; } = true; public bool EnableCustomShapes { get; set; } = true;
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public PcpSettings PcpSettings = new();
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
public int OptionGroupCollapsibleMin { get; set; } = 5; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
public int OptionGroupCollapsibleMin { get; set; } = 5;
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);

View file

@ -2,5 +2,6 @@ namespace Penumbra;
public class DebugConfiguration public class DebugConfiguration
{ {
public static bool WriteImcBytesToLog = false; public static bool WriteImcBytesToLog = false;
public static bool UseSkinMaterialProcessing = true;
} }

View file

@ -1,6 +1,7 @@
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.UI.AdvancedWindow.Materials;
using SharpGLTF.Materials; using SharpGLTF.Materials;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
@ -140,13 +141,13 @@ public class MaterialExporter
// Lerp between table row values to fetch final pixel values for each subtexture. // Lerp between table row values to fetch final pixel values for each subtexture.
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend); var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend);
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); baseColorSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedDiffuse), 1));
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend); var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend);
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); specularSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedSpecularColor), 1));
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend); var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend);
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); emissiveSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedEmissive), 1));
} }
} }
} }

View file

@ -360,11 +360,11 @@ public class MeshExporter
// (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range.
// TODO: While this assumption is safe, it would be sensible to actually check. // TODO: While this assumption is safe, it would be sensible to actually check.
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
return new VertexPositionNormalTangent( return new VertexPositionNormalTangent(
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)),
bitangent bitangent.SanitizeTangent()
); );
} }
@ -390,23 +390,30 @@ public class MeshExporter
} }
} }
usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours);
var nColors = colours?.Count ?? 0;
var materialUsages = ( var materialUsages = (
uvCount, uvCount,
usages.ContainsKey(MdlFile.VertexUsage.Color) nColors
); );
return materialUsages switch return materialUsages switch
{ {
(3, true) => typeof(VertexTexture3ColorFfxiv), (3, 2) => typeof(VertexTexture3Color2Ffxiv),
(3, false) => typeof(VertexTexture3), (3, 1) => typeof(VertexTexture3ColorFfxiv),
(2, true) => typeof(VertexTexture2ColorFfxiv), (3, 0) => typeof(VertexTexture3),
(2, false) => typeof(VertexTexture2), (2, 2) => typeof(VertexTexture2Color2Ffxiv),
(1, true) => typeof(VertexTexture1ColorFfxiv), (2, 1) => typeof(VertexTexture2ColorFfxiv),
(1, false) => typeof(VertexTexture1), (2, 0) => typeof(VertexTexture2),
(0, true) => typeof(VertexColorFfxiv), (1, 2) => typeof(VertexTexture1Color2Ffxiv),
(0, false) => typeof(VertexEmpty), (1, 1) => typeof(VertexTexture1ColorFfxiv),
(1, 0) => typeof(VertexTexture1),
(0, 2) => typeof(VertexColor2Ffxiv),
(0, 1) => typeof(VertexColorFfxiv),
(0, 0) => typeof(VertexEmpty),
_ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), _ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."),
}; };
} }
@ -419,6 +426,12 @@ public class MeshExporter
if (_materialType == typeof(VertexColorFfxiv)) if (_materialType == typeof(VertexColorFfxiv))
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
if (_materialType == typeof(VertexColor2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1));
}
if (_materialType == typeof(VertexTexture1)) if (_materialType == typeof(VertexTexture1))
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
@ -428,6 +441,16 @@ public class MeshExporter
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
); );
if (_materialType == typeof(VertexTexture1Color2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture1Color2Ffxiv(
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
ToVector4(color0),
ToVector4(color1)
);
}
// XIV packs two UVs into a single vec4 attribute. // XIV packs two UVs into a single vec4 attribute.
if (_materialType == typeof(VertexTexture2)) if (_materialType == typeof(VertexTexture2))
@ -448,6 +471,20 @@ public class MeshExporter
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
); );
} }
if (_materialType == typeof(VertexTexture2Color2Ffxiv))
{
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture2Color2Ffxiv(
new Vector2(uv.X, uv.Y),
new Vector2(uv.Z, uv.W),
ToVector4(color0),
ToVector4(color1)
);
}
if (_materialType == typeof(VertexTexture3)) if (_materialType == typeof(VertexTexture3))
{ {
// Not 100% sure about this // Not 100% sure about this
@ -472,6 +509,21 @@ public class MeshExporter
); );
} }
if (_materialType == typeof(VertexTexture3Color2Ffxiv))
{
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture3Color2Ffxiv(
new Vector2(uv0.X, uv0.Y),
new Vector2(uv0.Z, uv0.W),
new Vector2(uv1.X, uv1.Y),
ToVector4(color0),
ToVector4(color1)
);
}
throw _notifier.Exception($"Unknown material type {_skinningType}"); throw _notifier.Exception($"Unknown material type {_skinningType}");
} }
@ -537,6 +589,17 @@ public class MeshExporter
return list[0]; return list[0];
} }
/// <summary> Check that the list has length 2 for any case where this is expected and return both entries. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (T First, T Second) GetBothSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
{
var list = attributes[usage];
if (list.Count != 2)
throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2.");
return (list[0], list[1]);
}
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary> /// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private static Vector2 ToVector2(object data) private static Vector2 ToVector2(object data)

View file

@ -84,6 +84,103 @@ public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom
} }
} }
public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 0;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{ }
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
public Vector2 GetTexCoord(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetTexCoord(int setIndex, Vector2 coord)
{ }
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}
public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom
{ {
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes() public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
@ -172,6 +269,118 @@ public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) :
} }
} }
public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0 = texCoord0;
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 1;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
public Vector2 GetTexCoord(int index)
=> index switch
{
0 => TexCoord0,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex >= 1)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}
public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom
{ {
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes() public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
@ -266,6 +475,124 @@ public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec
} }
} }
public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0 = texCoord0;
public Vector2 TexCoord1 = texCoord1;
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 2;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
TexCoord1 += delta.TexCoord1Delta;
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
public Vector2 GetTexCoord(int index)
=> index switch
{
0 => TexCoord0,
1 => TexCoord1,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex == 1)
TexCoord1 = coord;
if (setIndex >= 2)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}
public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor)
: IVertexCustom : IVertexCustom
{ {
@ -367,3 +694,126 @@ public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec
throw new ArgumentOutOfRangeException(nameof(FfxivColor)); throw new ArgumentOutOfRangeException(nameof(FfxivColor));
} }
} }
public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1)
: IVertexCustom
{
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0 = texCoord0;
public Vector2 TexCoord1 = texCoord1;
public Vector2 TexCoord2 = texCoord2;
public Vector4 FfxivColor0 = ffxivColor0;
public Vector4 FfxivColor1 = ffxivColor1;
public int MaxColors
=> 0;
public int MaxTextCoords
=> 3;
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
public IEnumerable<string> CustomAttributes
=> CustomNames;
public void Add(in VertexMaterialDelta delta)
{
TexCoord0 += delta.TexCoord0Delta;
TexCoord1 += delta.TexCoord1Delta;
TexCoord2 += delta.TexCoord2Delta;
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
public Vector2 GetTexCoord(int index)
=> index switch
{
0 => TexCoord0,
1 => TexCoord1,
2 => TexCoord2,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex == 1)
TexCoord1 = coord;
if (setIndex == 2)
TexCoord2 = coord;
if (setIndex >= 3)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0":
value = FfxivColor0;
return true;
case "_FFXIV_COLOR_1":
value = FfxivColor1;
return true;
default:
value = null;
return false;
}
}
public void SetCustomAttribute(string attributeName, object value)
{
switch (attributeName)
{
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
FfxivColor0 = valueVector4;
break;
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
FfxivColor1 = valueVector4;
break;
}
}
public Vector4 GetColor(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{ }
public void Validate()
{
var components = new[]
{
FfxivColor0.X,
FfxivColor0.Y,
FfxivColor0.Z,
FfxivColor0.W,
};
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
components =
[
FfxivColor1.X,
FfxivColor1.Y,
FfxivColor1.Z,
FfxivColor1.W,
];
if (components.Any(component => component is < 0 or > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
}
}

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Rar; using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.SevenZip;
@ -146,6 +147,9 @@ public partial class TexToolsImporter
case ".mtrl": case ".mtrl":
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
break; break;
case ".tex":
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
break;
default: default:
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions); reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
break; break;

View file

@ -1,4 +1,4 @@
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;

View file

@ -259,6 +259,7 @@ public partial class TexToolsImporter
{ {
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data), ".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data),
".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data),
_ => data.Data, _ => data.Data,
}; };

View file

@ -1,4 +1,4 @@
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui; using OtterGui;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;

View file

@ -61,6 +61,76 @@ public static class TexFileParser
return 13; return 13;
} }
public static unsafe void FixMipOffsets(long size, ref TexFile.TexHeader header, out long newSize)
{
var width = (uint)header.Width;
var height = (uint)header.Height;
var format = header.Format.ToDXGI();
var bits = format.BitsPerPixel();
var totalSize = 80u;
size -= totalSize;
var minSize = format.IsCompressed() ? 4u : 1u;
for (var i = 0; i < 13; ++i)
{
var requiredSize = (uint)((long)width * height * bits / 8);
if (requiredSize > size)
{
newSize = totalSize;
if (header.MipCount != i)
{
Penumbra.Log.Debug(
$"-- Mip Map Count in TEX header was {header.MipCount}, but file only contains data for {i} Mip Maps, fixed.");
FixLodOffsets(ref header, i);
}
return;
}
if (header.OffsetToSurface[i] != totalSize)
{
Penumbra.Log.Debug(
$"-- Mip Map Offset {i + 1} in TEX header was {header.OffsetToSurface[i]} but should be {totalSize}, fixed.");
header.OffsetToSurface[i] = totalSize;
}
if (width == minSize && height == minSize)
{
++i;
newSize = totalSize + requiredSize;
if (header.MipCount != i)
{
Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints.");
FixLodOffsets(ref header, i);
}
return;
}
totalSize += requiredSize;
size -= requiredSize;
width = Math.Max(width / 2, minSize);
height = Math.Max(height / 2, minSize);
}
newSize = totalSize;
if (header.MipCount != 13)
{
Penumbra.Log.Debug($"-- Mip Map Count in TEX header was {header.MipCount}, but maximum is 13, fixed.");
FixLodOffsets(ref header, 13);
}
void FixLodOffsets(ref TexFile.TexHeader header, int index)
{
header.MipCount = index;
if (header.LodOffset[2] >= header.MipCount)
header.LodOffset[2] = (byte)(header.MipCount - 1);
if (header.LodOffset[1] >= header.MipCount)
header.LodOffset[1] = header.MipCount > 2 ? (byte)(header.MipCount - 2) : (byte)(header.MipCount - 1);
for (++index; index < 13; ++index)
header.OffsetToSurface[index] = 0;
}
}
private static unsafe void CopyData(ScratchImage image, BinaryReader r) private static unsafe void CopyData(ScratchImage image, BinaryReader r)
{ {
fixed (byte* ptr = image.Pixels) fixed (byte* ptr = image.Pixels)

View file

@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET;
using Lumina.Data.Files; using Lumina.Data.Files;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
@ -20,7 +20,7 @@ public static class TextureDrawer
{ {
size = texture.TextureWrap.Size.Contain(size); size = texture.TextureWrap.Size.Contain(size);
ImGui.Image(texture.TextureWrap.ImGuiHandle, size); ImGui.Image(texture.TextureWrap.Handle, size);
DrawData(texture); DrawData(texture);
} }
else if (texture.LoadError != null) else if (texture.LoadError != null)

View file

@ -406,7 +406,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
// See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition.
if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)
{ {
var device = uiBuilder.Device; var device = new Device(uiBuilder.DeviceHandle);
var dxgiDevice = device.QueryInterface<DxgiDevice>(); var dxgiDevice = device.QueryInterface<DxgiDevice>();
using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel);

View file

@ -0,0 +1,47 @@
namespace Penumbra.Interop;
public static unsafe partial class CloudApi
{
private const int CfSyncRootInfoBasic = 0;
/// <summary> Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. </summary>
/// <remarks> Can be expensive. Callers should cache the result when relevant. </remarks>
public static bool IsCloudSynced(string path)
{
var buffer = stackalloc long[1];
int hr;
uint length;
try
{
hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out length);
}
catch (DllNotFoundException)
{
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw DllNotFoundException");
return false;
}
catch (EntryPointNotFoundException)
{
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw EntryPointNotFoundException");
return false;
}
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT 0x{hr:X8}");
if (hr < 0)
return false;
if (length != sizeof(long))
{
Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes");
return false;
}
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}");
return true;
}
[LibraryImport("cldapi.dll", StringMarshalling = StringMarshalling.Utf16)]
private static partial int CfGetSyncRootInfoByPath(string filePath, int infoClass, void* infoBuffer, uint infoBufferLength,
out uint returnedLength);
}

View file

@ -59,9 +59,6 @@ public class GameState : IService
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true); private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
public ResolveData SoundData
=> _animationLoadData.Value;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public ResolveData SetSoundData(ResolveData data) public ResolveData SetSoundData(ResolveData data)
{ {

View file

@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
{ {
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_metaState = metaState; _metaState = metaState;
Task = hooks.CreateHook<Delegate>("Change Customize", Sigs.ChangeCustomize, Detour, !HookOverrides.Instance.Meta.ChangeCustomize); Task = hooks.CreateHook<Delegate>("Change Customize", Sigs.UpdateDrawData, Detour, !HookOverrides.Instance.Meta.ChangeCustomize);
} }
public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment);

View file

@ -35,14 +35,14 @@ public sealed unsafe class WeaponReload : EventWrapperPtr<DrawDataContainer, Cha
public bool Finished public bool Finished
=> _task.IsCompletedSuccessfully; => _task.IsCompletedSuccessfully;
private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g); private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h);
private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g) private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h)
{ {
var gameObject = drawData->OwnerObject; var gameObject = drawData->OwnerObject;
Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}."); Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}, {h}.");
Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon)); Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon));
_task.Result.Original(drawData, slot, weapon, d, e, f, g); _task.Result.Original(drawData, slot, weapon, d, e, f, g, h);
_postEvent.Invoke(drawData, gameObject); _postEvent.Invoke(drawData, gameObject);
} }

View file

@ -22,8 +22,8 @@ public sealed unsafe class AttributeHook : EventWrapper<Actor, Model, ModCollect
{ {
public enum Priority public enum Priority
{ {
/// <seealso cref="ShapeManager.OnAttributeComputed"/> /// <seealso cref="ShapeAttributeManager.OnAttributeComputed"/>
ShapeManager = 0, ShapeAttributeManager = 0,
} }
private readonly CollectionResolver _resolver; private readonly CollectionResolver _resolver;

View file

@ -63,7 +63,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi
if (!_framework.IsInFrameworkUpdateThread) if (!_framework.IsInFrameworkUpdateThread)
Penumbra.Log.Warning( Penumbra.Log.Warning(
$"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
try try
{ {

View file

@ -1,4 +1,5 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
@ -85,7 +86,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8); ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8);
private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId,
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, uint unk9); ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8,
uint unk9);
[Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))] [Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))]
private readonly Hook<GetResourceSyncPrototype> _getResourceSyncHook = null!; private readonly Hook<GetResourceSyncPrototype> _getResourceSyncHook = null!;
@ -118,18 +120,26 @@ public unsafe class ResourceService : IDisposable, IRequiredService
unk9); unk9);
} }
var original = gamePath; if (gamePath.IsEmpty)
{
Penumbra.Log.Error($"[ResourceService] Empty resource path requested with category {*categoryId}, type {*resourceType}, hash {*resourceHash}.");
return null;
}
var original = gamePath;
ResourceHandle* returnValue = null; ResourceHandle* returnValue = null;
ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync, ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync,
ref returnValue); ref returnValue);
if (returnValue != null) if (returnValue != null)
return returnValue; return returnValue;
return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, unk9); return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8,
unk9);
} }
/// <summary> Call the original GetResource function. </summary> /// <summary> Call the original GetResource function. </summary>
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, Utf8GamePath original, public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path,
Utf8GamePath original,
GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0)
{ {
var previous = _currentGetResourcePath.Value; var previous = _currentGetResourcePath.Value;
@ -141,7 +151,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
resourceParameters, unk8, unk9) resourceParameters, unk8, unk9)
: _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
resourceParameters, unk, unk8, unk9); resourceParameters, unk, unk8, unk9);
} finally }
finally
{ {
_currentGetResourcePath.Value = previous; _currentGetResourcePath.Value = previous;
} }
@ -163,7 +174,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
/// <param name="syncOriginal">The original game path of the resource, if loaded synchronously.</param> /// <param name="syncOriginal">The original game path of the resource, if loaded synchronously.</param>
/// <param name="previousState">The previous state of the resource.</param> /// <param name="previousState">The previous state of the resource.</param>
/// <param name="returnValue">The return value to use.</param> /// <param name="returnValue">The return value to use.</param>
public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue); public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal,
(byte UnkState, LoadState LoadState) previousState, ref uint returnValue);
/// <summary> /// <summary>
/// <inheritdoc cref="ResourceStateUpdatingDelegate"/> <para/> /// <inheritdoc cref="ResourceStateUpdatingDelegate"/> <para/>
@ -185,7 +197,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService
private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread) private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread)
{ {
var previousState = (handle->UnkState, handle->LoadState); var previousState = (handle->UnkState, handle->LoadState);
var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty; var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty;
ResourceStateUpdating?.Invoke(handle, syncOriginal); ResourceStateUpdating?.Invoke(handle, syncOriginal);
var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread); var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread);
ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret); ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret);

View file

@ -6,6 +6,7 @@ using Penumbra.Collections;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Processing;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
namespace Penumbra.Interop.Hooks.Resources; namespace Penumbra.Interop.Hooks.Resources;
@ -35,6 +36,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
private readonly Hook<MPapResolveDelegate> _resolveMPapPathHook; private readonly Hook<MPapResolveDelegate> _resolveMPapPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolveMdlPathHook; private readonly Hook<PerSlotResolveDelegate> _resolveMdlPathHook;
private readonly Hook<NamedResolveDelegate> _resolveMtrlPathHook; private readonly Hook<NamedResolveDelegate> _resolveMtrlPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolveSkinMtrlPathHook;
private readonly Hook<NamedResolveDelegate> _resolvePapPathHook; private readonly Hook<NamedResolveDelegate> _resolvePapPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolveKdbPathHook; private readonly Hook<PerSlotResolveDelegate> _resolveKdbPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolvePhybPathHook; private readonly Hook<PerSlotResolveDelegate> _resolvePhybPathHook;
@ -52,22 +54,23 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
{ {
_parent = parent; _parent = parent;
// @formatter:off // @formatter:off
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman); _resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman);
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman); _resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman);
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); _resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman);
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); _resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman);
_resolveKdbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman); _resolveKdbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman);
_vFunc81Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); _vFunc81Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81);
_resolveBnmbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman); _resolveBnmbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman);
_vFunc83Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83); _vFunc83Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83);
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); _resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman);
_resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); _resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb);
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap); _resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap);
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc); _resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc);
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl); _resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl);
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal); _resolveSkinMtrlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkinMtrl)}", hooks, vTable[91], ResolveSkinMtrl);
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); _resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal);
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); _resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman);
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid);
// @formatter:on // @formatter:on
@ -83,6 +86,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
_resolveMPapPathHook.Enable(); _resolveMPapPathHook.Enable();
_resolveMdlPathHook.Enable(); _resolveMdlPathHook.Enable();
_resolveMtrlPathHook.Enable(); _resolveMtrlPathHook.Enable();
_resolveSkinMtrlPathHook.Enable();
_resolvePapPathHook.Enable(); _resolvePapPathHook.Enable();
_resolveKdbPathHook.Enable(); _resolveKdbPathHook.Enable();
_resolvePhybPathHook.Enable(); _resolvePhybPathHook.Enable();
@ -103,6 +107,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
_resolveMPapPathHook.Disable(); _resolveMPapPathHook.Disable();
_resolveMdlPathHook.Disable(); _resolveMdlPathHook.Disable();
_resolveMtrlPathHook.Disable(); _resolveMtrlPathHook.Disable();
_resolveSkinMtrlPathHook.Disable();
_resolvePapPathHook.Disable(); _resolvePapPathHook.Disable();
_resolveKdbPathHook.Disable(); _resolveKdbPathHook.Disable();
_resolvePhybPathHook.Disable(); _resolvePhybPathHook.Disable();
@ -123,6 +128,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
_resolveMPapPathHook.Dispose(); _resolveMPapPathHook.Dispose();
_resolveMdlPathHook.Dispose(); _resolveMdlPathHook.Dispose();
_resolveMtrlPathHook.Dispose(); _resolveMtrlPathHook.Dispose();
_resolveSkinMtrlPathHook.Dispose();
_resolvePapPathHook.Dispose(); _resolvePapPathHook.Dispose();
_resolveKdbPathHook.Dispose(); _resolveKdbPathHook.Dispose();
_resolvePhybPathHook.Dispose(); _resolvePhybPathHook.Dispose();
@ -153,6 +159,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName) private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName)
=> ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName)); => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName));
private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex)
{
var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex);
if (DebugConfiguration.UseSkinMaterialProcessing && finalPathBuffer != nint.Zero && finalPathBuffer == pathBuffer)
SkinMtrlPathEarlyProcessing.Process(new Span<byte>((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex);
return ResolvePath(drawObject, finalPathBuffer);
}
private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName)
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));

View file

@ -85,7 +85,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
if (mtrlHandle == null) if (mtrlHandle == null)
continue; continue;
PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _); PathDataHandler.Split(mtrlHandle->FileName.AsSpan(), out var path, out _);
var fileName = CiByteString.FromSpanUnsafe(path, true); var fileName = CiByteString.FromSpanUnsafe(path, true);
if (fileName == needle) if (fileName == needle)
result.Add(new MaterialInfo(index, type, i, j)); result.Add(new MaterialInfo(index, type, i, j));

View file

@ -137,7 +137,7 @@ public sealed unsafe class CollectionResolver(
{ {
var item = charaEntry.Value; var item = charaEntry.Value;
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
Penumbra.Log.Verbose( Penumbra.Log.Excessive(
$"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}."); $"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}.");
if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll) if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll)
{ {

View file

@ -75,6 +75,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable
return false; return false;
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
_objects.InvokeRequiredUpdates();
return true; return true;
} }

View file

@ -0,0 +1,7 @@
namespace Penumbra.Interop;
public static partial class ProcessThreadApi
{
[LibraryImport("kernel32.dll")]
public static partial uint GetCurrentThreadId();
}

View file

@ -0,0 +1,119 @@
using Dalamud.Game;
using Dalamud.Plugin.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.String;
namespace Penumbra.Interop.Processing;
public sealed class PbdFilePostProcessor : IFilePostProcessor
{
private readonly IFileAllocator _allocator;
private byte[] _epbdData;
private unsafe delegate* unmanaged<ResourceHandle*, void> _loadEpbdData;
public ResourceType Type
=> ResourceType.Pbd;
public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner)
{
_allocator = allocator;
_epbdData = SetEpbdData(dataManager);
_loadEpbdData = (delegate* unmanaged<ResourceHandle*, void>)scanner.ScanText(Sigs.LoadEpbdData);
}
public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan<byte> additionalData)
{
if (_epbdData.Length is 0)
return;
if (resource->LoadState is not LoadState.Success)
{
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} failed load ({resource->LoadState}).");
return;
}
var (data, length) = resource->GetData();
if (length is 0 || data == nint.Zero)
{
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} succeeded load but has no data.");
return;
}
var span = new ReadOnlySpan<byte>((void*)data, (int)resource->FileSize);
var reader = new PackReader(span);
if (reader.HasData)
{
Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {resource->FileName()} with EPBD data.");
return;
}
var newData = AppendData(span);
fixed (byte* ptr = newData)
{
// Set the appended data and the actual file size, then re-load the EPBD data via game function call.
if (resource->SetData((nint)ptr, newData.Length))
{
resource->FileSize = (uint)newData.Length;
resource->CsHandle.FileSize2 = (uint)newData.Length;
resource->CsHandle.FileSize3 = (uint)newData.Length;
_loadEpbdData(resource);
// Free original data.
_allocator.Release((void*)data, length);
Penumbra.Log.Debug($"[ResourceLoader] Loaded {resource->FileName()} from file and appended default EPBD data.");
}
else
{
Penumbra.Log.Warning(
$"[ResourceLoader] Failed to append EPBD data to custom PBD at {resource->FileName()}.");
}
}
}
/// <summary> Combine the given data with the default PBD data using the game's file allocator. </summary>
private unsafe ReadOnlySpan<byte> AppendData(ReadOnlySpan<byte> data)
{
// offset has to be set, otherwise not called.
var newLength = data.Length + _epbdData.Length;
var memory = _allocator.Allocate(newLength);
var span = new Span<byte>(memory, newLength);
data.CopyTo(span);
_epbdData.CopyTo(span[data.Length..]);
return span;
}
/// <summary> Fetch the default EPBD data from the .pbd file of the game's installation. </summary>
private static byte[] SetEpbdData(IDataManager dataManager)
{
try
{
var file = dataManager.GetFile(GamePaths.Pbd.Path);
if (file is null || file.Data.Length is 0)
{
Penumbra.Log.Warning("Default PBD file has no data.");
return [];
}
ReadOnlySpan<byte> span = file.Data;
var reader = new PackReader(span);
if (!reader.HasData)
{
Penumbra.Log.Warning("Default PBD file has no EPBD section.");
return [];
}
var offset = span.Length - (int)reader.PackLength;
var ret = span[offset..];
Penumbra.Log.Verbose($"Default PBD file has EPBD section of length {ret.Length} at offset {offset}.");
return ret.ToArray();
}
catch (Exception ex)
{
Penumbra.Log.Error($"Unknown error getting default EPBD data:\n{ex}");
return [];
}
}
}

View file

@ -0,0 +1,63 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
namespace Penumbra.Interop.Processing;
public static unsafe class SkinMtrlPathEarlyProcessing
{
public static void Process(Span<byte> path, CharacterBase* character, uint slotIndex)
{
var end = path.IndexOf(MaterialExtension());
if (end < 0)
return;
var suffixPos = path[..end].LastIndexOf((byte)'_');
if (suffixPos < 0)
return;
var handle = GetModelResourceHandle(character, slotIndex);
if (handle == null)
return;
var skinSuffix = GetSkinSuffix(handle);
if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7)
return;
++suffixPos;
skinSuffix.CopyTo(path[suffixPos..]);
suffixPos += skinSuffix.Length;
MaterialExtension().CopyTo(path[suffixPos..]);
return;
static ReadOnlySpan<byte> MaterialExtension()
=> ".mtrl\0"u8;
}
private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex)
{
if (character is null)
return null;
if (character->PerSlotStagingArea is not null)
{
var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle;
if (handle != null)
return handle;
}
var model = character->Models[slotIndex];
return model is null ? null : model->ModelResourceHandle;
}
private static ReadOnlySpan<byte> GetSkinSuffix(ModelResourceHandle* handle)
{
foreach (var (attribute, _) in handle->Attributes)
{
var attributeSpan = attribute.AsSpan();
if (attributeSpan.Length > 12 && attributeSpan[..11].SequenceEqual("skin_suffix"u8) && attributeSpan[11] is (byte)'=' or (byte)'_')
return attributeSpan[12..];
}
return [];
}
}

View file

@ -338,6 +338,34 @@ internal partial record ResolveContext
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
} }
private Utf8GamePath ResolveKineDriverModulePath(uint partialSkeletonIndex)
{
// Correctness and Safety:
// Resolving a KineDriver module path through the game's code can use EST metadata for human skeletons.
// Additionally, it can dereference null pointers for human equipment skeletons.
return ModelType switch
{
ModelType.Human => ResolveHumanKineDriverModulePath(partialSkeletonIndex),
_ => ResolveKineDriverModulePathNative(partialSkeletonIndex),
};
}
private Utf8GamePath ResolveHumanKineDriverModulePath(uint partialSkeletonIndex)
{
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
if (set.Id is 0)
return Utf8GamePath.Empty;
var path = GamePaths.Kdb.Customization(raceCode, slot, set);
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolveKineDriverModulePathNative(uint partialSkeletonIndex)
{
var path = CharacterBase->ResolveKdbPathAsByteString(partialSkeletonIndex);
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc) private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc)
{ {
var animation = ResolveImcData(imc).MaterialAnimationId; var animation = ResolveImcData(imc).MaterialAnimationId;

View file

@ -59,7 +59,7 @@ internal unsafe partial record ResolveContext(
if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path)) if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path))
return null; return null;
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, (ResourceHandle*)resourceHandle, path);
} }
[SkipLocalsInit] [SkipLocalsInit]
@ -188,7 +188,8 @@ internal unsafe partial record ResolveContext(
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath); return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath);
} }
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle) public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle,
MaterialResourceHandle* skinMtrlHandle, ResourceHandle* mpapHandle)
{ {
if (mdl is null || mdl->ModelResourceHandle is null) if (mdl is null || mdl->ModelResourceHandle is null)
return null; return null;
@ -218,6 +219,12 @@ internal unsafe partial record ResolveContext(
} }
} }
if (skinMtrlHandle is not null
&& Utf8GamePath.FromByteString(CharacterBase->ResolveSkinMtrlPathAsByteString(SlotIndex), out var skinMtrlPath)
&& CreateNodeFromMaterial(skinMtrlHandle->Material, skinMtrlPath) is
{ } skinMaaterialNode)
node.Children.Add(skinMaaterialNode);
if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode) if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode)
node.Children.Add(decalNode); node.Children.Add(decalNode);
@ -238,7 +245,7 @@ internal unsafe partial record ResolveContext(
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
return cached; return cached;
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, (ResourceHandle*)resource, path, false);
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value)); var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value));
if (shpkNode is not null) if (shpkNode is not null)
{ {
@ -364,7 +371,8 @@ internal unsafe partial record ResolveContext(
return node; return node;
} }
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex) public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, ResourceHandle* kdbHandle,
uint partialSkeletonIndex)
{ {
if (sklb is null || sklb->SkeletonResourceHandle is null) if (sklb is null || sklb->SkeletonResourceHandle is null)
return null; return null;
@ -379,6 +387,8 @@ internal unsafe partial record ResolveContext(
node.Children.Add(skpNode); node.Children.Add(skpNode);
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode) if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
node.Children.Add(phybNode); node.Children.Add(phybNode);
if (CreateNodeFromKdb(kdbHandle, partialSkeletonIndex) is { } kdbNode)
node.Children.Add(kdbNode);
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
return node; return node;
@ -420,6 +430,24 @@ internal unsafe partial record ResolveContext(
return node; return node;
} }
private ResourceNode? CreateNodeFromKdb(ResourceHandle* kdbHandle, uint partialSkeletonIndex)
{
if (kdbHandle is null)
return null;
var path = ResolveKineDriverModulePath(partialSkeletonIndex);
if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached))
return cached;
var node = CreateNode(ResourceType.Kdb, 0, kdbHandle, path, false);
if (Global.WithUiData)
node.FallbackName = "KineDriver Module";
Global.Nodes.Add((path, (nint)kdbHandle), node);
return node;
}
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
{ {
var path = gamePath.Path.Split((byte)'/'); var path = gamePath.Path.Split((byte)'/');

View file

@ -45,7 +45,9 @@ public class ResourceNode : ICloneable
/// <summary> Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). </summary> /// <summary> Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). </summary>
public bool Protected public bool Protected
=> ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd; => ForceProtected
|| Internal
|| Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Skp or ResourceType.Phyb or ResourceType.Kdb or ResourceType.Pbd;
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
{ {

View file

@ -70,12 +70,16 @@ public class ResourceTree(
var genericContext = globalContext.CreateContext(model); var genericContext = globalContext.CreateContext(model);
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = model->MaterialAnimationPacks;
var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948);
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, model->SlotCount) : []; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, model->SlotCount) : [];
var skinMtrlArray = modelType switch
{
ModelType.Human => ((Human*) model)->SlotSkinMaterials,
_ => [],
};
var decalArray = modelType switch var decalArray = modelType switch
{ {
ModelType.Human => human->SlotDecalsSpan, ModelType.Human => human->SlotDecals,
ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals, ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals,
ModelType.Weapon => [((Weapon*)model)->Decal], ModelType.Weapon => [((Weapon*)model)->Decal],
ModelType.Monster => [((Monster*)model)->Decal], ModelType.Monster => [((Monster*)model)->Decal],
@ -108,7 +112,8 @@ public class ResourceTree(
var mdl = model->Models[i]; var mdl = model->Models[i];
if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode) i < skinMtrlArray.Length ? skinMtrlArray[(int)i].Value : null, i < mpapArray.Length ? mpapArray[(int)i].Value : null) is
{ } mdlNode)
{ {
if (globalContext.WithUiData) if (globalContext.WithUiData)
mdlNode.FallbackName = $"Model #{i}"; mdlNode.FallbackName = $"Model #{i}";
@ -116,9 +121,8 @@ public class ResourceTree(
} }
} }
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule); AddSkeleton(Nodes, genericContext, model);
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton);
AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940));
AddWeapons(globalContext, model); AddWeapons(globalContext, model);
@ -149,8 +153,7 @@ public class ResourceTree(
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = subObject->MaterialAnimationPacks;
var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948);
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : []; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : [];
for (var i = 0; i < subObject->SlotCount; ++i) for (var i = 0; i < subObject->SlotCount; ++i)
@ -166,7 +169,8 @@ public class ResourceTree(
} }
var mdl = subObject->Models[i]; var mdl = subObject->Models[i];
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode) if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, null, i < mpapArray.Length ? mpapArray[i].Value : null) is
{ } mdlNode)
{ {
if (globalContext.WithUiData) if (globalContext.WithUiData)
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}"; mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
@ -174,10 +178,8 @@ public class ResourceTree(
} }
} }
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, AddSkeleton(weaponNodes, genericContext, subObject, $"Weapon #{weaponIndex}, ");
$"Weapon #{weaponIndex}, "); AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton,
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940),
$"Weapon #{weaponIndex}, "); $"Weapon #{weaponIndex}, ");
++weaponIndex; ++weaponIndex;
@ -239,8 +241,11 @@ public class ResourceTree(
} }
} }
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, CharacterBase* model, string prefix = "")
=> AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, model->BoneKineDriverModule, prefix);
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
string prefix = "") BoneKineDriverModule* kineDriver, string prefix = "")
{ {
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
if (eidNode != null) if (eidNode != null)
@ -255,9 +260,9 @@ public class ResourceTree(
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
{ {
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null;
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
{ {
if (context.Global.WithUiData) if (context.Global.WithUiData)
sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; sklbNode.FallbackName = $"{prefix}Skeleton #{i}";

View file

@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable
return; return;
foreach (ref var f in currentTerritory->Furniture) foreach (ref var f in currentTerritory->FurnitureManager.FurnitureMemory)
{ {
var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null; var gameObject = f.Index >= 0 ? currentTerritory->FurnitureManager.ObjectManager.ObjectArray.Objects[f.Index].Value : null;
if (gameObject == null) if (gameObject == null)
continue; continue;

View file

@ -1,3 +1,4 @@
using Dalamud.Bindings.ImGui;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using OtterGui.Services; using OtterGui.Services;
using SharpDX.Direct3D; using SharpDX.Direct3D;
@ -16,7 +17,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = []; private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = [];
/// <remarks> Caching this across frames will cause a crash to desktop. </remarks> /// <remarks> Caching this across frames will cause a crash to desktop. </remarks>
public nint GetImGuiHandle(Texture* texture, byte sliceIndex) public ImTextureID GetImGuiHandle(Texture* texture, byte sliceIndex)
{ {
if (texture == null) if (texture == null)
throw new ArgumentNullException(nameof(texture)); throw new ArgumentNullException(nameof(texture));
@ -25,7 +26,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state))
{ {
state.Refresh(); state.Refresh();
return (nint)state.ShaderResourceView; return new ImTextureID((nint)state.ShaderResourceView);
} }
var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView;
var description = srv.Description; var description = srv.Description;
@ -60,7 +61,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
} }
state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description));
_activeSlices.Add(((nint)texture, sliceIndex), state); _activeSlices.Add(((nint)texture, sliceIndex), state);
return (nint)state.ShaderResourceView; return new ImTextureID((nint)state.ShaderResourceView);
} }
public void Tick() public void Tick()

View file

@ -10,28 +10,34 @@ internal static class StructExtensions
public static CiByteString AsByteString(in this StdString str) public static CiByteString AsByteString(in this StdString str)
=> CiByteString.FromSpanUnsafe(str.AsSpan(), true); => CiByteString.FromSpanUnsafe(str.AsSpan(), true);
public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character) public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character)
{ {
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); return ToOwnedByteString(character.ResolveEidPath(pathBuffer));
}
public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex)
{
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex));
} }
public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex)
{ {
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex));
} }
public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex)
{ {
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex));
}
public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName)
{
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
}
public static unsafe CiByteString ResolveSkinMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex)
{
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveSkinMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex));
} }
public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId) public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId)
@ -40,16 +46,16 @@ internal static class StructExtensions
return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId)); return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId));
} }
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
{ {
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex));
} }
public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
{ {
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
} }
public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
@ -58,6 +64,12 @@ internal static class StructExtensions
return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex)); return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex));
} }
public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
{
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveKdbPath(pathBuffer, CharacterBase.PathBufferSize, partialSkeletonIndex));
}
private static unsafe CiByteString ToOwnedByteString(CStringPointer str) private static unsafe CiByteString ToOwnedByteString(CStringPointer str)
=> str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty; => str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty;

View file

@ -0,0 +1,145 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeAttributeString Attribute, GenderRace GenderRaceCondition)
: IComparable<AtrIdentifier>, IMetaIdentifier
{
public int CompareTo(AtrIdentifier other)
{
var slotComparison = Slot.CompareTo(other.Slot);
if (slotComparison is not 0)
return slotComparison;
if (Id.HasValue)
{
if (other.Id.HasValue)
{
var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id);
if (idComparison is not 0)
return idComparison;
}
else
{
return -1;
}
}
else if (other.Id.HasValue)
{
return 1;
}
var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition);
if (genderRaceComparison is not 0)
return genderRaceComparison;
return Attribute.CompareTo(other.Attribute);
}
public override string ToString()
{
var sb = new StringBuilder(64);
sb.Append("Shp - ")
.Append(Attribute);
if (Slot is HumanSlot.Unknown)
{
sb.Append(" - All Slots & IDs");
}
else
{
sb.Append(" - ")
.Append(Slot.ToName())
.Append(" - ");
if (Id.HasValue)
sb.Append(Id.Value.Id);
else
sb.Append("All IDs");
}
if (GenderRaceCondition is not GenderRace.Unknown)
sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode());
return sb.ToString();
}
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{
// Nothing for now since it depends entirely on the shape key.
}
public MetaIndex FileIndex()
=> (MetaIndex)(-1);
public bool Validate()
{
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
return false;
if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition))
return false;
if (Slot is HumanSlot.Unknown && Id is not null)
return false;
if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue })
return false;
if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 })
return false;
return Attribute.ValidateCustomAttributeString();
}
public JObject AddToJson(JObject jObj)
{
if (Slot is not HumanSlot.Unknown)
jObj["Slot"] = Slot.ToString();
if (Id.HasValue)
jObj["Id"] = Id.Value.Id.ToString();
jObj["Attribute"] = Attribute.ToString();
if (GenderRaceCondition is not GenderRace.Unknown)
jObj["GenderRaceCondition"] = (uint)GenderRaceCondition;
return jObj;
}
public static AtrIdentifier? FromJson(JObject jObj)
{
var attribute = jObj["Attribute"]?.ToObject<string>();
if (attribute is null || !ShapeAttributeString.TryRead(attribute, out var attributeString))
return null;
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
var id = jObj["Id"]?.ToObject<ushort>();
var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject<GenderRace>() ?? 0;
var identifier = new AtrIdentifier(slot, id, attributeString, genderRaceCondition);
return identifier.Validate() ? identifier : null;
}
public MetaManipulationType Type
=> MetaManipulationType.Atr;
}
[JsonConverter(typeof(Converter))]
public readonly record struct AtrEntry(bool Value)
{
public static readonly AtrEntry True = new(true);
public static readonly AtrEntry False = new(false);
private class Converter : JsonConverter<AtrEntry>
{
public override void WriteJson(JsonWriter writer, AtrEntry value, JsonSerializer serializer)
=> serializer.Serialize(writer, value.Value);
public override AtrEntry ReadJson(JsonReader reader, Type objectType, AtrEntry existingValue, bool hasExistingValue,
JsonSerializer serializer)
=> new(serializer.Deserialize<bool>(reader));
}
}

View file

@ -16,10 +16,10 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
if (!Enum.IsDefined(Type)) if (!Enum.IsDefined(Type))
return false; return false;
if (Type is GlobalEqpType.DoNotHideVieraHats or GlobalEqpType.DoNotHideHrothgarHats) if (Type.HasCondition())
return Condition == 0; return Condition.Id is not 0;
return Condition != 0; return Condition.Id is 0;
} }
public JObject AddToJson(JObject jObj) public JObject AddToJson(JObject jObj)
@ -89,6 +89,12 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName()); changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName());
else if (Type is GlobalEqpType.DoNotHideHrothgarHats) else if (Type is GlobalEqpType.DoNotHideHrothgarHats)
changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName()); changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName());
else if (Type is GlobalEqpType.HideHorns)
changedItems.UpdateCountOrSet("All Au Ra Horns", () => new IdentifiedName());
else if (Type is GlobalEqpType.HideVieraEars)
changedItems.UpdateCountOrSet("All Viera Ears", () => new IdentifiedName());
else if (Type is GlobalEqpType.HideMiqoteEars)
changedItems.UpdateCountOrSet("All Miqo'te Ears", () => new IdentifiedName());
} }
public MetaIndex FileIndex() public MetaIndex FileIndex()

View file

@ -13,6 +13,9 @@ public enum GlobalEqpType
DoNotHideRingL, DoNotHideRingL,
DoNotHideHrothgarHats, DoNotHideHrothgarHats,
DoNotHideVieraHats, DoNotHideVieraHats,
HideHorns,
HideVieraEars,
HideMiqoteEars,
} }
public static class GlobalEqpExtensions public static class GlobalEqpExtensions
@ -27,6 +30,9 @@ public static class GlobalEqpExtensions
GlobalEqpType.DoNotHideRingL => true, GlobalEqpType.DoNotHideRingL => true,
GlobalEqpType.DoNotHideHrothgarHats => false, GlobalEqpType.DoNotHideHrothgarHats => false,
GlobalEqpType.DoNotHideVieraHats => false, GlobalEqpType.DoNotHideVieraHats => false,
GlobalEqpType.HideHorns => false,
GlobalEqpType.HideVieraEars => false,
GlobalEqpType.HideMiqoteEars => false,
_ => false, _ => false,
}; };
@ -41,6 +47,9 @@ public static class GlobalEqpExtensions
GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8, GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8,
GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8, GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8,
GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8, GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8,
GlobalEqpType.HideHorns => "Always Hide Horns (Au Ra)"u8,
GlobalEqpType.HideVieraEars => "Always Hide Ears (Viera)"u8,
GlobalEqpType.HideMiqoteEars => "Always Hide Ears (Miqo'te)"u8,
_ => "\0"u8, _ => "\0"u8,
}; };
@ -60,6 +69,9 @@ public static class GlobalEqpExtensions
"Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8, "Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8,
GlobalEqpType.DoNotHideVieraHats => GlobalEqpType.DoNotHideVieraHats =>
"Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8, "Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8,
_ => "\0"u8, GlobalEqpType.HideHorns => "Forces the game to hide Au Ra horns regardless of headwear."u8,
GlobalEqpType.HideVieraEars => "Forces the game to hide Viera ears regardless of headwear."u8,
GlobalEqpType.HideMiqoteEars => "Forces the game to hide Miqo'te ears regardless of headwear."u8,
_ => "\0"u8,
}; };
} }

View file

@ -16,6 +16,7 @@ public enum MetaManipulationType : byte
GlobalEqp = 7, GlobalEqp = 7,
Atch = 8, Atch = 8,
Shp = 9, Shp = 9,
Atr = 10,
} }
public interface IMetaIdentifier public interface IMetaIdentifier

View file

@ -1,7 +1,10 @@
using System.Collections.Frozen;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Util; using Penumbra.Util;
using ImcEntry = Penumbra.GameData.Structs.ImcEntry; using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
@ -11,129 +14,333 @@ namespace Penumbra.Meta.Manipulations;
[JsonConverter(typeof(Converter))] [JsonConverter(typeof(Converter))]
public class MetaDictionary public class MetaDictionary
{ {
private readonly Dictionary<ImcIdentifier, ImcEntry> _imc = []; private class Wrapper : HashSet<GlobalEqpManipulation>
private readonly Dictionary<EqpIdentifier, EqpEntryInternal> _eqp = []; {
private readonly Dictionary<EqdpIdentifier, EqdpEntryInternal> _eqdp = []; public readonly Dictionary<ImcIdentifier, ImcEntry> Imc = [];
private readonly Dictionary<EstIdentifier, EstEntry> _est = []; public readonly Dictionary<EqpIdentifier, EqpEntryInternal> Eqp = [];
private readonly Dictionary<RspIdentifier, RspEntry> _rsp = []; public readonly Dictionary<EqdpIdentifier, EqdpEntryInternal> Eqdp = [];
private readonly Dictionary<GmpIdentifier, GmpEntry> _gmp = []; public readonly Dictionary<EstIdentifier, EstEntry> Est = [];
private readonly Dictionary<AtchIdentifier, AtchEntry> _atch = []; public readonly Dictionary<RspIdentifier, RspEntry> Rsp = [];
private readonly Dictionary<ShpIdentifier, ShpEntry> _shp = []; public readonly Dictionary<GmpIdentifier, GmpEntry> Gmp = [];
private readonly HashSet<GlobalEqpManipulation> _globalEqp = []; public readonly Dictionary<AtchIdentifier, AtchEntry> Atch = [];
public readonly Dictionary<ShpIdentifier, ShpEntry> Shp = [];
public readonly Dictionary<AtrIdentifier, AtrEntry> Atr = [];
public Wrapper()
{ }
public Wrapper(MetaCache cache)
{
Imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
Eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
Est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Atr = cache.Atr.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
foreach (var geqp in cache.GlobalEqp.Keys)
Add(geqp);
}
public static unsafe Wrapper Filtered(MetaCache cache, Actor actor)
{
if (!actor.IsCharacter)
return new Wrapper(cache);
var model = actor.Model;
if (!model.IsHuman)
return new Wrapper(cache);
var headId = model.GetModelId(HumanSlot.Head);
var bodyId = model.GetModelId(HumanSlot.Body);
var equipIdSet = ((IEnumerable<PrimaryId>)
[
headId,
bodyId,
model.GetModelId(HumanSlot.Hands),
model.GetModelId(HumanSlot.Legs),
model.GetModelId(HumanSlot.Feet),
]).ToFrozenSet();
var earsId = model.GetModelId(HumanSlot.Ears);
var neckId = model.GetModelId(HumanSlot.Neck);
var wristId = model.GetModelId(HumanSlot.Wrists);
var rFingerId = model.GetModelId(HumanSlot.RFinger);
var lFingerId = model.GetModelId(HumanSlot.LFinger);
var wrapper = new Wrapper();
// Check for all relevant primary IDs due to slot overlap.
foreach (var (eqp, value) in cache.Eqp)
{
if (eqp.Slot.IsEquipment())
{
if (equipIdSet.Contains(eqp.SetId))
wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot));
}
else
{
switch (eqp.Slot)
{
case EquipSlot.Ears when eqp.SetId == earsId:
case EquipSlot.Neck when eqp.SetId == neckId:
case EquipSlot.Wrists when eqp.SetId == wristId:
case EquipSlot.RFinger when eqp.SetId == rFingerId:
case EquipSlot.LFinger when eqp.SetId == lFingerId:
wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot));
break;
}
}
}
// Check also for body IDs due to body occupying head.
foreach (var (gmp, value) in cache.Gmp)
{
if (gmp.SetId == headId || gmp.SetId == bodyId)
wrapper.Gmp.Add(gmp, value.Entry);
}
// Check for all races due to inheritance and all slots due to overlap.
foreach (var (eqdp, value) in cache.Eqdp)
{
if (eqdp.Slot.IsEquipment())
{
if (equipIdSet.Contains(eqdp.SetId))
wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot));
}
else
{
switch (eqdp.Slot)
{
case EquipSlot.Ears when eqdp.SetId == earsId:
case EquipSlot.Neck when eqdp.SetId == neckId:
case EquipSlot.Wrists when eqdp.SetId == wristId:
case EquipSlot.RFinger when eqdp.SetId == rFingerId:
case EquipSlot.LFinger when eqdp.SetId == lFingerId:
wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot));
break;
}
}
}
var genderRace = (GenderRace)model.AsHuman->RaceSexId;
var hairId = model.GetModelId(HumanSlot.Hair);
var faceId = model.GetModelId(HumanSlot.Face);
// We do not need to care for racial inheritance for ESTs.
foreach (var (est, value) in cache.Est)
{
switch (est.Slot)
{
case EstType.Hair when est.SetId == hairId && est.GenderRace == genderRace:
case EstType.Face when est.SetId == faceId && est.GenderRace == genderRace:
case EstType.Body when est.SetId == bodyId && est.GenderRace == genderRace:
case EstType.Head when (est.SetId == headId || est.SetId == bodyId) && est.GenderRace == genderRace:
wrapper.Est.Add(est, value.Entry);
break;
}
}
foreach (var (geqp, _) in cache.GlobalEqp)
{
switch (geqp.Type)
{
case GlobalEqpType.DoNotHideEarrings when geqp.Condition != earsId:
case GlobalEqpType.DoNotHideNecklace when geqp.Condition != neckId:
case GlobalEqpType.DoNotHideBracelets when geqp.Condition != wristId:
case GlobalEqpType.DoNotHideRingR when geqp.Condition != rFingerId:
case GlobalEqpType.DoNotHideRingL when geqp.Condition != lFingerId:
continue;
default: wrapper.Add(geqp); break;
}
}
var (_, _, main, off) = model.GetWeapons(actor);
foreach (var (imc, value) in cache.Imc)
{
switch (imc.ObjectType)
{
case ObjectType.Equipment when equipIdSet.Contains(imc.PrimaryId): wrapper.Imc.Add(imc, value.Entry); break;
case ObjectType.Weapon:
if (imc.PrimaryId == main.Skeleton && imc.SecondaryId == main.Weapon)
wrapper.Imc.Add(imc, value.Entry);
else if (imc.PrimaryId == off.Skeleton && imc.SecondaryId == off.Weapon)
wrapper.Imc.Add(imc, value.Entry);
break;
case ObjectType.Accessory:
switch (imc.EquipSlot)
{
case EquipSlot.Ears when imc.PrimaryId == earsId:
case EquipSlot.Neck when imc.PrimaryId == neckId:
case EquipSlot.Wrists when imc.PrimaryId == wristId:
case EquipSlot.RFinger when imc.PrimaryId == rFingerId:
case EquipSlot.LFinger when imc.PrimaryId == lFingerId:
wrapper.Imc.Add(imc, value.Entry);
break;
}
break;
}
}
var subRace = (SubRace)model.AsHuman->Customize[4];
foreach (var (rsp, value) in cache.Rsp)
{
if (rsp.SubRace == subRace)
wrapper.Rsp.Add(rsp, value.Entry);
}
// Keep all atch, atr and shp.
wrapper.Atch.EnsureCapacity(cache.Atch.Count);
wrapper.Shp.EnsureCapacity(cache.Shp.Count);
wrapper.Atr.EnsureCapacity(cache.Atr.Count);
foreach (var (atch, value) in cache.Atch)
wrapper.Atch.Add(atch, value.Entry);
foreach (var (shp, value) in cache.Shp)
wrapper.Shp.Add(shp, value.Entry);
foreach (var (atr, value) in cache.Atr)
wrapper.Atr.Add(atr, value.Entry);
return wrapper;
}
}
private Wrapper? _data;
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
=> _imc; => _data?.Imc ?? [];
public IReadOnlyDictionary<EqpIdentifier, EqpEntryInternal> Eqp public IReadOnlyDictionary<EqpIdentifier, EqpEntryInternal> Eqp
=> _eqp; => _data?.Eqp ?? [];
public IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> Eqdp public IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> Eqdp
=> _eqdp; => _data?.Eqdp ?? [];
public IReadOnlyDictionary<EstIdentifier, EstEntry> Est public IReadOnlyDictionary<EstIdentifier, EstEntry> Est
=> _est; => _data?.Est ?? [];
public IReadOnlyDictionary<GmpIdentifier, GmpEntry> Gmp public IReadOnlyDictionary<GmpIdentifier, GmpEntry> Gmp
=> _gmp; => _data?.Gmp ?? [];
public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp
=> _rsp; => _data?.Rsp ?? [];
public IReadOnlyDictionary<AtchIdentifier, AtchEntry> Atch public IReadOnlyDictionary<AtchIdentifier, AtchEntry> Atch
=> _atch; => _data?.Atch ?? [];
public IReadOnlyDictionary<ShpIdentifier, ShpEntry> Shp public IReadOnlyDictionary<ShpIdentifier, ShpEntry> Shp
=> _shp; => _data?.Shp ?? [];
public IReadOnlyDictionary<AtrIdentifier, AtrEntry> Atr
=> _data?.Atr ?? [];
public IReadOnlySet<GlobalEqpManipulation> GlobalEqp public IReadOnlySet<GlobalEqpManipulation> GlobalEqp
=> _globalEqp; => _data ?? [];
public int Count { get; private set; } public int Count { get; private set; }
public int GetCount(MetaManipulationType type) public int GetCount(MetaManipulationType type)
=> type switch => _data is null
{ ? 0
MetaManipulationType.Imc => _imc.Count, : type switch
MetaManipulationType.Eqdp => _eqdp.Count, {
MetaManipulationType.Eqp => _eqp.Count, MetaManipulationType.Imc => _data.Imc.Count,
MetaManipulationType.Est => _est.Count, MetaManipulationType.Eqdp => _data.Eqdp.Count,
MetaManipulationType.Gmp => _gmp.Count, MetaManipulationType.Eqp => _data.Eqp.Count,
MetaManipulationType.Rsp => _rsp.Count, MetaManipulationType.Est => _data.Est.Count,
MetaManipulationType.Atch => _atch.Count, MetaManipulationType.Gmp => _data.Gmp.Count,
MetaManipulationType.Shp => _shp.Count, MetaManipulationType.Rsp => _data.Rsp.Count,
MetaManipulationType.GlobalEqp => _globalEqp.Count, MetaManipulationType.Atch => _data.Atch.Count,
_ => 0, MetaManipulationType.Shp => _data.Shp.Count,
}; MetaManipulationType.Atr => _data.Atr.Count,
MetaManipulationType.GlobalEqp => _data.Count,
_ => 0,
};
public bool Contains(IMetaIdentifier identifier) public bool Contains(IMetaIdentifier identifier)
=> identifier switch => _data is not null
{ && identifier switch
EqdpIdentifier i => _eqdp.ContainsKey(i), {
EqpIdentifier i => _eqp.ContainsKey(i), EqdpIdentifier i => _data.Eqdp.ContainsKey(i),
EstIdentifier i => _est.ContainsKey(i), EqpIdentifier i => _data.Eqp.ContainsKey(i),
GlobalEqpManipulation i => _globalEqp.Contains(i), EstIdentifier i => _data.Est.ContainsKey(i),
GmpIdentifier i => _gmp.ContainsKey(i), GlobalEqpManipulation i => _data.Contains(i),
ImcIdentifier i => _imc.ContainsKey(i), GmpIdentifier i => _data.Gmp.ContainsKey(i),
AtchIdentifier i => _atch.ContainsKey(i), ImcIdentifier i => _data.Imc.ContainsKey(i),
ShpIdentifier i => _shp.ContainsKey(i), AtchIdentifier i => _data.Atch.ContainsKey(i),
RspIdentifier i => _rsp.ContainsKey(i), ShpIdentifier i => _data.Shp.ContainsKey(i),
_ => false, AtrIdentifier i => _data.Atr.ContainsKey(i),
}; RspIdentifier i => _data.Rsp.ContainsKey(i),
_ => false,
};
public void Clear() public void Clear()
{ {
_data = null;
Count = 0; Count = 0;
_imc.Clear();
_eqp.Clear();
_eqdp.Clear();
_est.Clear();
_rsp.Clear();
_gmp.Clear();
_atch.Clear();
_shp.Clear();
_globalEqp.Clear();
} }
public void ClearForDefault() public void ClearForDefault()
{ {
Count = _globalEqp.Count; if (_data is null)
_imc.Clear(); return;
_eqp.Clear();
_eqdp.Clear(); if (_data.Count is 0 && Shp.Count is 0 && Atr.Count is 0)
_est.Clear(); {
_rsp.Clear(); _data = null;
_gmp.Clear(); Count = 0;
_atch.Clear(); return;
}
Count = GlobalEqp.Count + Shp.Count + Atr.Count;
_data!.Imc.Clear();
_data!.Eqp.Clear();
_data!.Eqdp.Clear();
_data!.Est.Clear();
_data!.Rsp.Clear();
_data!.Gmp.Clear();
_data!.Atch.Clear();
} }
public bool Equals(MetaDictionary other) public bool Equals(MetaDictionary other)
=> Count == other.Count {
&& _imc.SetEquals(other._imc) if (Count != other.Count)
&& _eqp.SetEquals(other._eqp) return false;
&& _eqdp.SetEquals(other._eqdp)
&& _est.SetEquals(other._est) if (_data is null)
&& _rsp.SetEquals(other._rsp) return true;
&& _gmp.SetEquals(other._gmp)
&& _atch.SetEquals(other._atch) return _data.Imc.SetEquals(other._data!.Imc)
&& _shp.SetEquals(other._shp) && _data.Eqp.SetEquals(other._data!.Eqp)
&& _globalEqp.SetEquals(other._globalEqp); && _data.Eqdp.SetEquals(other._data!.Eqdp)
&& _data.Est.SetEquals(other._data!.Est)
&& _data.Rsp.SetEquals(other._data!.Rsp)
&& _data.Gmp.SetEquals(other._data!.Gmp)
&& _data.Atch.SetEquals(other._data!.Atch)
&& _data.Shp.SetEquals(other._data!.Shp)
&& _data.Atr.SetEquals(other._data!.Atr)
&& _data.SetEquals(other._data!);
}
public IEnumerable<IMetaIdentifier> Identifiers public IEnumerable<IMetaIdentifier> Identifiers
=> _imc.Keys.Cast<IMetaIdentifier>() => _data is null
.Concat(_eqdp.Keys.Cast<IMetaIdentifier>()) ? []
.Concat(_eqp.Keys.Cast<IMetaIdentifier>()) : _data.Imc.Keys.Cast<IMetaIdentifier>()
.Concat(_est.Keys.Cast<IMetaIdentifier>()) .Concat(_data!.Eqdp.Keys.Cast<IMetaIdentifier>())
.Concat(_gmp.Keys.Cast<IMetaIdentifier>()) .Concat(_data!.Eqp.Keys.Cast<IMetaIdentifier>())
.Concat(_rsp.Keys.Cast<IMetaIdentifier>()) .Concat(_data!.Est.Keys.Cast<IMetaIdentifier>())
.Concat(_atch.Keys.Cast<IMetaIdentifier>()) .Concat(_data!.Gmp.Keys.Cast<IMetaIdentifier>())
.Concat(_shp.Keys.Cast<IMetaIdentifier>()) .Concat(_data!.Rsp.Keys.Cast<IMetaIdentifier>())
.Concat(_globalEqp.Cast<IMetaIdentifier>()); .Concat(_data!.Atch.Keys.Cast<IMetaIdentifier>())
.Concat(_data!.Shp.Keys.Cast<IMetaIdentifier>())
.Concat(_data!.Atr.Keys.Cast<IMetaIdentifier>())
.Concat(_data!.Cast<IMetaIdentifier>());
#region TryAdd #region TryAdd
public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) public bool TryAdd(ImcIdentifier identifier, ImcEntry entry)
{ {
if (!_imc.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Imc.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -142,7 +349,8 @@ public class MetaDictionary
public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry)
{ {
if (!_eqp.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Eqp.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -154,7 +362,8 @@ public class MetaDictionary
public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry)
{ {
if (!_eqdp.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Eqdp.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -166,7 +375,8 @@ public class MetaDictionary
public bool TryAdd(EstIdentifier identifier, EstEntry entry) public bool TryAdd(EstIdentifier identifier, EstEntry entry)
{ {
if (!_est.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Est.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -175,7 +385,8 @@ public class MetaDictionary
public bool TryAdd(GmpIdentifier identifier, GmpEntry entry) public bool TryAdd(GmpIdentifier identifier, GmpEntry entry)
{ {
if (!_gmp.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Gmp.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -184,7 +395,8 @@ public class MetaDictionary
public bool TryAdd(RspIdentifier identifier, RspEntry entry) public bool TryAdd(RspIdentifier identifier, RspEntry entry)
{ {
if (!_rsp.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Rsp.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -193,7 +405,8 @@ public class MetaDictionary
public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry) public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry)
{ {
if (!_atch.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Atch.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -202,7 +415,18 @@ public class MetaDictionary
public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry) public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry)
{ {
if (!_shp.TryAdd(identifier, entry)) _data ??= [];
if (!_data!.Shp.TryAdd(identifier, entry))
return false;
++Count;
return true;
}
public bool TryAdd(AtrIdentifier identifier, in AtrEntry entry)
{
_data ??= [];
if (!_data!.Atr.TryAdd(identifier, entry))
return false; return false;
++Count; ++Count;
@ -211,7 +435,8 @@ public class MetaDictionary
public bool TryAdd(GlobalEqpManipulation identifier) public bool TryAdd(GlobalEqpManipulation identifier)
{ {
if (!_globalEqp.Add(identifier)) _data ??= [];
if (!_data.Add(identifier))
return false; return false;
++Count; ++Count;
@ -224,19 +449,19 @@ public class MetaDictionary
public bool Update(ImcIdentifier identifier, ImcEntry entry) public bool Update(ImcIdentifier identifier, ImcEntry entry)
{ {
if (!_imc.ContainsKey(identifier)) if (_data is null || !_data.Imc.ContainsKey(identifier))
return false; return false;
_imc[identifier] = entry; _data.Imc[identifier] = entry;
return true; return true;
} }
public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) public bool Update(EqpIdentifier identifier, EqpEntryInternal entry)
{ {
if (!_eqp.ContainsKey(identifier)) if (_data is null || !_data.Eqp.ContainsKey(identifier))
return false; return false;
_eqp[identifier] = entry; _data.Eqp[identifier] = entry;
return true; return true;
} }
@ -245,10 +470,10 @@ public class MetaDictionary
public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry)
{ {
if (!_eqdp.ContainsKey(identifier)) if (_data is null || !_data.Eqdp.ContainsKey(identifier))
return false; return false;
_eqdp[identifier] = entry; _data.Eqdp[identifier] = entry;
return true; return true;
} }
@ -257,46 +482,55 @@ public class MetaDictionary
public bool Update(EstIdentifier identifier, EstEntry entry) public bool Update(EstIdentifier identifier, EstEntry entry)
{ {
if (!_est.ContainsKey(identifier)) if (_data is null || !_data.Est.ContainsKey(identifier))
return false; return false;
_est[identifier] = entry; _data.Est[identifier] = entry;
return true; return true;
} }
public bool Update(GmpIdentifier identifier, GmpEntry entry) public bool Update(GmpIdentifier identifier, GmpEntry entry)
{ {
if (!_gmp.ContainsKey(identifier)) if (_data is null || !_data.Gmp.ContainsKey(identifier))
return false; return false;
_gmp[identifier] = entry; _data.Gmp[identifier] = entry;
return true; return true;
} }
public bool Update(RspIdentifier identifier, RspEntry entry) public bool Update(RspIdentifier identifier, RspEntry entry)
{ {
if (!_rsp.ContainsKey(identifier)) if (_data is null || !_data.Rsp.ContainsKey(identifier))
return false; return false;
_rsp[identifier] = entry; _data.Rsp[identifier] = entry;
return true; return true;
} }
public bool Update(AtchIdentifier identifier, in AtchEntry entry) public bool Update(AtchIdentifier identifier, in AtchEntry entry)
{ {
if (!_atch.ContainsKey(identifier)) if (_data is null || !_data.Atch.ContainsKey(identifier))
return false; return false;
_atch[identifier] = entry; _data.Atch[identifier] = entry;
return true; return true;
} }
public bool Update(ShpIdentifier identifier, in ShpEntry entry) public bool Update(ShpIdentifier identifier, in ShpEntry entry)
{ {
if (!_shp.ContainsKey(identifier)) if (_data is null || !_data.Shp.ContainsKey(identifier))
return false; return false;
_shp[identifier] = entry; _data.Shp[identifier] = entry;
return true;
}
public bool Update(AtrIdentifier identifier, in AtrEntry entry)
{
if (_data is null || !_data.Atr.ContainsKey(identifier))
return false;
_data.Atr[identifier] = entry;
return true; return true;
} }
@ -305,48 +539,63 @@ public class MetaDictionary
#region TryGetValue #region TryGetValue
public bool TryGetValue(EstIdentifier identifier, out EstEntry value) public bool TryGetValue(EstIdentifier identifier, out EstEntry value)
=> _est.TryGetValue(identifier, out value); => _data?.Est.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value)
=> _eqp.TryGetValue(identifier, out value); => _data?.Eqp.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value)
=> _eqdp.TryGetValue(identifier, out value); => _data?.Eqdp.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value)
=> _gmp.TryGetValue(identifier, out value); => _data?.Gmp.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(RspIdentifier identifier, out RspEntry value) public bool TryGetValue(RspIdentifier identifier, out RspEntry value)
=> _rsp.TryGetValue(identifier, out value); => _data?.Rsp.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value)
=> _imc.TryGetValue(identifier, out value); => _data?.Imc.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value)
=> _atch.TryGetValue(identifier, out value); => _data?.Atch.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value)
=> _shp.TryGetValue(identifier, out value); => _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(AtrIdentifier identifier, out AtrEntry value)
=> _data?.Atr.TryGetValue(identifier, out value) ?? SetDefault(out value);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool SetDefault<T>(out T? value)
{
value = default;
return false;
}
#endregion #endregion
public bool Remove(IMetaIdentifier identifier) public bool Remove(IMetaIdentifier identifier)
{ {
if (_data is null)
return false;
var ret = identifier switch var ret = identifier switch
{ {
EqdpIdentifier i => _eqdp.Remove(i), EqdpIdentifier i => _data.Eqdp.Remove(i),
EqpIdentifier i => _eqp.Remove(i), EqpIdentifier i => _data.Eqp.Remove(i),
EstIdentifier i => _est.Remove(i), EstIdentifier i => _data.Est.Remove(i),
GlobalEqpManipulation i => _globalEqp.Remove(i), GlobalEqpManipulation i => _data.Remove(i),
GmpIdentifier i => _gmp.Remove(i), GmpIdentifier i => _data.Gmp.Remove(i),
ImcIdentifier i => _imc.Remove(i), ImcIdentifier i => _data.Imc.Remove(i),
RspIdentifier i => _rsp.Remove(i), RspIdentifier i => _data.Rsp.Remove(i),
AtchIdentifier i => _atch.Remove(i), AtchIdentifier i => _data.Atch.Remove(i),
ShpIdentifier i => _shp.Remove(i), ShpIdentifier i => _data.Shp.Remove(i),
AtrIdentifier i => _data.Atr.Remove(i),
_ => false, _ => false,
}; };
if (ret) if (ret && --Count is 0)
--Count; _data = null;
return ret; return ret;
} }
@ -354,86 +603,106 @@ public class MetaDictionary
public void UnionWith(MetaDictionary manips) public void UnionWith(MetaDictionary manips)
{ {
foreach (var (identifier, entry) in manips._imc) if (manips.Count is 0)
return;
_data ??= [];
foreach (var (identifier, entry) in manips._data!.Imc)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._eqp) foreach (var (identifier, entry) in manips._data!.Eqp)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._eqdp) foreach (var (identifier, entry) in manips._data!.Eqdp)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._gmp) foreach (var (identifier, entry) in manips._data!.Gmp)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._rsp) foreach (var (identifier, entry) in manips._data!.Rsp)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._est) foreach (var (identifier, entry) in manips._data!.Est)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._atch) foreach (var (identifier, entry) in manips._data!.Atch)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._shp) foreach (var (identifier, entry) in manips._data!.Shp)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var identifier in manips._globalEqp) foreach (var (identifier, entry) in manips._data!.Atr)
TryAdd(identifier, entry);
foreach (var identifier in manips._data!)
TryAdd(identifier); TryAdd(identifier);
} }
/// <summary> Try to merge all manipulations from manips into this, and return the first failure, if any. </summary> /// <summary> Try to merge all manipulations from manips into this, and return the first failure, if any. </summary>
public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier) public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier)
{ {
foreach (var (identifier, _) in manips._imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) if (manips.Count is 0)
{
failedIdentifier = null;
return true;
}
_data ??= [];
foreach (var (identifier, _) in manips._data!.Imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var (identifier, _) in manips._eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) foreach (var (identifier, _) in manips._data!.Eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var (identifier, _) in manips._eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) foreach (var (identifier, _) in manips._data!.Eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var (identifier, _) in manips._gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) foreach (var (identifier, _) in manips._data!.Gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var (identifier, _) in manips._rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) foreach (var (identifier, _) in manips._data!.Rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var (identifier, _) in manips._est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) foreach (var (identifier, _) in manips._data!.Est.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var (identifier, _) in manips._atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) foreach (var (identifier, _) in manips._data!.Atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var (identifier, _) in manips._shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) foreach (var (identifier, _) in manips._data!.Shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
} }
foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) foreach (var (identifier, _) in manips._data!.Atr.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{
failedIdentifier = identifier;
return false;
}
foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
return false; return false;
@ -445,30 +714,53 @@ public class MetaDictionary
public void SetTo(MetaDictionary other) public void SetTo(MetaDictionary other)
{ {
_imc.SetTo(other._imc); if (other.Count is 0)
_eqp.SetTo(other._eqp); {
_eqdp.SetTo(other._eqdp); _data = null;
_est.SetTo(other._est); Count = 0;
_rsp.SetTo(other._rsp); return;
_gmp.SetTo(other._gmp); }
_atch.SetTo(other._atch);
_shp.SetTo(other._shp); _data ??= [];
_globalEqp.SetTo(other._globalEqp); _data!.Imc.SetTo(other._data!.Imc);
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; _data!.Eqp.SetTo(other._data!.Eqp);
_data!.Eqdp.SetTo(other._data!.Eqdp);
_data!.Est.SetTo(other._data!.Est);
_data!.Rsp.SetTo(other._data!.Rsp);
_data!.Gmp.SetTo(other._data!.Gmp);
_data!.Atch.SetTo(other._data!.Atch);
_data!.Shp.SetTo(other._data!.Shp);
_data!.Atr.SetTo(other._data!.Atr);
_data!.SetTo(other._data!);
Count = other.Count;
} }
public void UpdateTo(MetaDictionary other) public void UpdateTo(MetaDictionary other)
{ {
_imc.UpdateTo(other._imc); if (other.Count is 0)
_eqp.UpdateTo(other._eqp); return;
_eqdp.UpdateTo(other._eqdp);
_est.UpdateTo(other._est); _data ??= [];
_rsp.UpdateTo(other._rsp); _data!.Imc.UpdateTo(other._data!.Imc);
_gmp.UpdateTo(other._gmp); _data!.Eqp.UpdateTo(other._data!.Eqp);
_atch.UpdateTo(other._atch); _data!.Eqdp.UpdateTo(other._data!.Eqdp);
_shp.UpdateTo(other._shp); _data!.Est.UpdateTo(other._data!.Est);
_globalEqp.UnionWith(other._globalEqp); _data!.Rsp.UpdateTo(other._data!.Rsp);
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; _data!.Gmp.UpdateTo(other._data!.Gmp);
_data!.Atch.UpdateTo(other._data!.Atch);
_data!.Shp.UpdateTo(other._data!.Shp);
_data!.Atr.UpdateTo(other._data!.Atr);
_data!.UnionWith(other._data!);
Count = _data!.Imc.Count
+ _data!.Eqp.Count
+ _data!.Eqdp.Count
+ _data!.Est.Count
+ _data!.Rsp.Count
+ _data!.Gmp.Count
+ _data!.Atch.Count
+ _data!.Shp.Count
+ _data!.Atr.Count
+ _data!.Count;
} }
#endregion #endregion
@ -566,6 +858,16 @@ public class MetaDictionary
}), }),
}; };
public static JObject Serialize(AtrIdentifier identifier, AtrEntry entry)
=> new()
{
["Type"] = MetaManipulationType.Atr.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = entry.Value,
}),
};
public static JObject Serialize(GlobalEqpManipulation identifier) public static JObject Serialize(GlobalEqpManipulation identifier)
=> new() => new()
{ {
@ -597,6 +899,8 @@ public class MetaDictionary
return Serialize(Unsafe.As<TIdentifier, AtchIdentifier>(ref identifier), Unsafe.As<TEntry, AtchEntry>(ref entry)); return Serialize(Unsafe.As<TIdentifier, AtchIdentifier>(ref identifier), Unsafe.As<TEntry, AtchEntry>(ref entry));
if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry)) if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry))
return Serialize(Unsafe.As<TIdentifier, ShpIdentifier>(ref identifier), Unsafe.As<TEntry, ShpEntry>(ref entry)); return Serialize(Unsafe.As<TIdentifier, ShpIdentifier>(ref identifier), Unsafe.As<TEntry, ShpEntry>(ref entry));
if (typeof(TIdentifier) == typeof(AtrIdentifier) && typeof(TEntry) == typeof(AtrEntry))
return Serialize(Unsafe.As<TIdentifier, AtrIdentifier>(ref identifier), Unsafe.As<TEntry, AtrEntry>(ref entry));
if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) if (typeof(TIdentifier) == typeof(GlobalEqpManipulation))
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier)); return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier));
@ -635,15 +939,20 @@ public class MetaDictionary
} }
var array = new JArray(); var array = new JArray();
SerializeTo(array, value._imc); if (value._data is not null)
SerializeTo(array, value._eqp); {
SerializeTo(array, value._eqdp); SerializeTo(array, value._data!.Imc);
SerializeTo(array, value._est); SerializeTo(array, value._data!.Eqp);
SerializeTo(array, value._rsp); SerializeTo(array, value._data!.Eqdp);
SerializeTo(array, value._gmp); SerializeTo(array, value._data!.Est);
SerializeTo(array, value._atch); SerializeTo(array, value._data!.Rsp);
SerializeTo(array, value._shp); SerializeTo(array, value._data!.Gmp);
SerializeTo(array, value._globalEqp); SerializeTo(array, value._data!.Atch);
SerializeTo(array, value._data!.Shp);
SerializeTo(array, value._data!.Atr);
SerializeTo(array, value._data!);
}
array.WriteTo(writer); array.WriteTo(writer);
} }
@ -750,6 +1059,16 @@ public class MetaDictionary
Penumbra.Log.Warning("Invalid SHP Manipulation encountered."); Penumbra.Log.Warning("Invalid SHP Manipulation encountered.");
break; break;
} }
case MetaManipulationType.Atr:
{
var identifier = AtrIdentifier.FromJson(manip);
var entry = new AtrEntry(manip["Entry"]?.Value<bool>() ?? true);
if (identifier.HasValue)
dict.TryAdd(identifier.Value, entry);
else
Penumbra.Log.Warning("Invalid ATR Manipulation encountered.");
break;
}
case MetaManipulationType.GlobalEqp: case MetaManipulationType.GlobalEqp:
{ {
var identifier = GlobalEqpManipulation.FromJson(manip); var identifier = GlobalEqpManipulation.FromJson(manip);
@ -771,18 +1090,30 @@ public class MetaDictionary
public MetaDictionary(MetaCache? cache) public MetaDictionary(MetaCache? cache)
{ {
if (cache == null) if (cache is null)
return; return;
_imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _data = new Wrapper(cache);
_eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); Count = cache.Count;
_eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); }
_est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
_gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); public MetaDictionary(MetaCache? cache, Actor actor)
_rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); {
_atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); if (cache is null)
_shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); return;
_globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet();
Count = cache.Count; _data = Wrapper.Filtered(cache, actor);
Count = _data.Count
+ _data.Eqp.Count
+ _data.Eqdp.Count
+ _data.Est.Count
+ _data.Gmp.Count
+ _data.Imc.Count
+ _data.Rsp.Count
+ _data.Atch.Count
+ _data.Atr.Count
+ _data.Shp.Count;
if (Count is 0)
_data = null;
} }
} }

View file

@ -1,13 +1,30 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Manipulations;
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition) [JsonConverter(typeof(StringEnumConverter))]
public enum ShapeConnectorCondition : byte
{
None = 0,
Wrists = 1,
Waist = 2,
Ankles = 3,
}
public readonly record struct ShpIdentifier(
HumanSlot Slot,
PrimaryId? Id,
ShapeAttributeString Shape,
ShapeConnectorCondition ConnectorCondition,
GenderRace GenderRaceCondition)
: IComparable<ShpIdentifier>, IMetaIdentifier : IComparable<ShpIdentifier>, IMetaIdentifier
{ {
public int CompareTo(ShpIdentifier other) public int CompareTo(ShpIdentifier other)
@ -34,11 +51,15 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
return 1; return 1;
} }
var shapeComparison = Shape.CompareTo(other.Shape); var conditionComparison = ConnectorCondition.CompareTo(other.ConnectorCondition);
if (shapeComparison is not 0) if (conditionComparison is not 0)
return shapeComparison; return conditionComparison;
return ShapeCondition.CompareTo(other.ShapeCondition); var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition);
if (genderRaceComparison is not 0)
return genderRaceComparison;
return Shape.CompareTo(other.Shape);
} }
@ -62,9 +83,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
sb.Append("All IDs"); sb.Append("All IDs");
} }
if (ShapeCondition.Length > 0) switch (ConnectorCondition)
sb.Append(" - ") {
.Append(ShapeCondition); case ShapeConnectorCondition.Wrists: sb.Append(" - Wrist Connector"); break;
case ShapeConnectorCondition.Waist: sb.Append(" - Waist Connector"); break;
case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break;
}
if (GenderRaceCondition is not GenderRace.Unknown)
sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode());
return sb.ToString(); return sb.ToString();
} }
@ -81,63 +109,34 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
return false; return false;
if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition))
return false;
if (!Enum.IsDefined(ConnectorCondition))
return false;
if (Slot is HumanSlot.Unknown && Id is not null) if (Slot is HumanSlot.Unknown && Id is not null)
return false; return false;
if (!ValidateCustomShapeString(Shape)) if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue })
return false; return false;
if (ShapeCondition.Length is 0) if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 })
return true;
if (!ValidateCustomShapeString(ShapeCondition))
return false; return false;
return Slot switch if (!Shape.ValidateCustomShapeString())
return false;
return ConnectorCondition switch
{ {
HumanSlot.Hands when ShapeCondition.IsWrist() => true, ShapeConnectorCondition.None => true,
HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true, ShapeConnectorCondition.Wrists => Slot is HumanSlot.Body or HumanSlot.Hands or HumanSlot.Unknown,
HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, ShapeConnectorCondition.Waist => Slot is HumanSlot.Body or HumanSlot.Legs or HumanSlot.Unknown,
HumanSlot.Feet when ShapeCondition.IsAnkle() => true, ShapeConnectorCondition.Ankles => Slot is HumanSlot.Legs or HumanSlot.Feet or HumanSlot.Unknown,
HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, _ => false,
_ => false,
}; };
} }
public static unsafe bool ValidateCustomShapeString(byte* shape)
{
// "shpx_*"
if (shape is null)
return false;
if (*shape++ is not (byte)'s'
|| *shape++ is not (byte)'h'
|| *shape++ is not (byte)'p'
|| *shape++ is not (byte)'x'
|| *shape++ is not (byte)'_'
|| *shape is 0)
return false;
return true;
}
public static bool ValidateCustomShapeString(in ShapeString shape)
{
// "shpx_*"
if (shape.Length < 6)
return false;
var span = shape.AsSpan;
if (span[0] is not (byte)'s'
|| span[1] is not (byte)'h'
|| span[2] is not (byte)'p'
|| span[3] is not (byte)'x'
|| span[4] is not (byte)'_')
return false;
return true;
}
public JObject AddToJson(JObject jObj) public JObject AddToJson(JObject jObj)
{ {
if (Slot is not HumanSlot.Unknown) if (Slot is not HumanSlot.Unknown)
@ -145,22 +144,24 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (Id.HasValue) if (Id.HasValue)
jObj["Id"] = Id.Value.Id.ToString(); jObj["Id"] = Id.Value.Id.ToString();
jObj["Shape"] = Shape.ToString(); jObj["Shape"] = Shape.ToString();
if (ShapeCondition.Length > 0) if (ConnectorCondition is not ShapeConnectorCondition.None)
jObj["ShapeCondition"] = ShapeCondition.ToString(); jObj["ConnectorCondition"] = ConnectorCondition.ToString();
if (GenderRaceCondition is not GenderRace.Unknown)
jObj["GenderRaceCondition"] = (uint)GenderRaceCondition;
return jObj; return jObj;
} }
public static ShpIdentifier? FromJson(JObject jObj) public static ShpIdentifier? FromJson(JObject jObj)
{ {
var shape = jObj["Shape"]?.ToObject<string>(); var shape = jObj["Shape"]?.ToObject<string>();
if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) if (shape is null || !ShapeAttributeString.TryRead(shape, out var shapeString))
return null; return null;
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown; var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
var id = jObj["Id"]?.ToObject<ushort>(); var id = jObj["Id"]?.ToObject<ushort>();
var shapeCondition = jObj["ShapeCondition"]?.ToObject<string>(); var connectorCondition = jObj["ConnectorCondition"]?.ToObject<ShapeConnectorCondition>() ?? ShapeConnectorCondition.None;
var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s; var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject<GenderRace>() ?? 0;
var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString); var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition, genderRaceCondition);
return identifier.Validate() ? identifier : null; return identifier.Validate() ? identifier : null;
} }

View file

@ -0,0 +1,227 @@
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta;
public unsafe class ShapeAttributeManager : IRequiredService, IDisposable
{
public const int NumSlots = 14;
public const int ModelSlotSize = 18;
private readonly AttributeHook _attributeHook;
public static ReadOnlySpan<HumanSlot> UsedModels
=>
[
HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists,
HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear,
];
public ShapeAttributeManager(AttributeHook attributeHook)
{
_attributeHook = attributeHook;
_attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeAttributeManager);
}
private readonly Dictionary<ShapeAttributeString, short>[] _temporaryShapes =
Enumerable.Range(0, NumSlots).Select(_ => new Dictionary<ShapeAttributeString, short>()).ToArray();
private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize];
private HumanSlot _modelIndex;
private int _slotIndex;
private GenderRace _genderRace;
private FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* _model;
public void Dispose()
=> _attributeHook.Unsubscribe(OnAttributeComputed);
private void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
{
if (!collection.HasCache)
return;
_genderRace = (GenderRace)model.AsHuman->RaceSexId;
for (_slotIndex = 0; _slotIndex < NumSlots; ++_slotIndex)
{
_modelIndex = UsedModels[_slotIndex];
_model = model.AsHuman->Models[_modelIndex.ToIndex()];
if (_model is null || _model->ModelResourceHandle is null)
continue;
_ids[(int)_modelIndex] = model.GetModelId(_modelIndex);
CheckShapes(collection.MetaCache!.Shp);
CheckAttributes(collection.MetaCache!.Atr);
if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears)
AccessoryImcCheck(model);
}
UpdateDefaultMasks(model, collection.MetaCache!.Shp);
}
private void AccessoryImcCheck(Model model)
{
var imcMask = (ushort)(0x03FF & *(ushort*)(model.Address + 0xAAC + 6 * (int)_modelIndex));
Span<byte> attr =
[
(byte)'a',
(byte)'t',
(byte)'r',
(byte)'_',
AccessoryByte(_modelIndex),
(byte)'v',
(byte)'_',
(byte)'a',
0,
];
for (var i = 1; i < 10; ++i)
{
var flag = (ushort)(1 << i);
if ((imcMask & flag) is not 0)
continue;
attr[^2] = (byte)('a' + i);
foreach (var (attribute, index) in _model->ModelResourceHandle->Attributes)
{
if (!EqualAttribute(attr, attribute.Value))
continue;
_model->EnabledAttributeIndexMask &= ~(1u << index);
break;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
private static bool EqualAttribute(Span<byte> needle, byte* haystack)
{
foreach (var character in needle)
{
if (*haystack++ != character)
return false;
}
return true;
}
private static byte AccessoryByte(HumanSlot slot)
=> slot switch
{
HumanSlot.Head => (byte)'m',
HumanSlot.Ears => (byte)'e',
HumanSlot.Neck => (byte)'n',
HumanSlot.Wrists => (byte)'w',
HumanSlot.RFinger => (byte)'r',
HumanSlot.LFinger => (byte)'r',
_ => 0,
};
private void CheckAttributes(AtrCache attributeCache)
{
if (attributeCache.DisabledCount is 0)
return;
ref var attributes = ref _model->ModelResourceHandle->Attributes;
foreach (var (attribute, index) in attributes.Where(kvp => ShapeAttributeString.ValidateCustomAttributeString(kvp.Key.Value)))
{
if (ShapeAttributeString.TryRead(attribute.Value, out var attributeString))
{
// Mask out custom attributes if they are disabled. Attributes are enabled by default.
if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace))
_model->EnabledAttributeIndexMask &= ~(1u << index);
}
else
{
Penumbra.Log.Warning($"Trying to read a attribute string that is too long: {attribute}.");
}
}
}
private void CheckShapes(ShpCache shapeCache)
{
_temporaryShapes[_slotIndex].Clear();
ref var shapes = ref _model->ModelResourceHandle->Shapes;
foreach (var (shape, index) in shapes.Where(kvp => ShapeAttributeString.ValidateCustomShapeString(kvp.Key.Value)))
{
if (ShapeAttributeString.TryRead(shape.Value, out var shapeString))
{
_temporaryShapes[_slotIndex].TryAdd(shapeString, index);
// Add custom shapes if they are enabled. Shapes are disabled by default.
if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace))
_model->EnabledShapeKeyIndexMask |= 1u << index;
}
else
{
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
}
}
}
private void UpdateDefaultMasks(Model human, ShpCache cache)
{
var genderRace = (GenderRace)human.AsHuman->RaceSexId;
foreach (var (shape, topIndex) in _temporaryShapes[1])
{
if (shape.IsWrist()
&& _temporaryShapes[2].TryGetValue(shape, out var handIndex)
&& !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace)
&& !cache.ShouldBeDisabled(shape, HumanSlot.Hands, _ids[2], genderRace)
&& human.AsHuman->Models[1] is not null
&& human.AsHuman->Models[2] is not null)
{
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Wrists), genderRace, HumanSlot.Body, HumanSlot.Hands, 1, 2);
}
if (shape.IsWaist()
&& _temporaryShapes[3].TryGetValue(shape, out var legIndex)
&& !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace)
&& !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace)
&& human.AsHuman->Models[1] is not null
&& human.AsHuman->Models[3] is not null)
{
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Waist), genderRace, HumanSlot.Body, HumanSlot.Legs, 1, 3);
}
}
foreach (var (shape, bottomIndex) in _temporaryShapes[3])
{
if (shape.IsAnkle()
&& _temporaryShapes[4].TryGetValue(shape, out var footIndex)
&& !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace)
&& !cache.ShouldBeDisabled(shape, HumanSlot.Feet, _ids[4], genderRace)
&& human.AsHuman->Models[3] is not null
&& human.AsHuman->Models[4] is not null)
{
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex;
human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Ankles), genderRace, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
}
}
return;
void CheckCondition(IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> dict, GenderRace genderRace, HumanSlot slot1,
HumanSlot slot2, int idx1, int idx2)
{
foreach (var (shape, set) in dict)
{
if (set.CheckEntry(slot1, _ids[idx1], genderRace) is true && _temporaryShapes[idx1].TryGetValue(shape, out var index1))
human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1;
if (set.CheckEntry(slot2, _ids[idx2], genderRace) is true && _temporaryShapes[idx2].TryGetValue(shape, out var index2))
human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2;
}
}
}
}

View file

@ -6,11 +6,11 @@ using Penumbra.String.Functions;
namespace Penumbra.Meta; namespace Penumbra.Meta;
[JsonConverter(typeof(Converter))] [JsonConverter(typeof(Converter))]
public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString> public struct ShapeAttributeString : IEquatable<ShapeAttributeString>, IComparable<ShapeAttributeString>
{ {
public const int MaxLength = 30; public const int MaxLength = 30;
public static readonly ShapeString Empty = new(); public static readonly ShapeAttributeString Empty = new();
private FixedString32 _buffer; private FixedString32 _buffer;
@ -37,6 +37,72 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
} }
} }
public static unsafe bool ValidateCustomShapeString(byte* shape)
{
// "shpx_*"
if (shape is null)
return false;
if (*shape++ is not (byte)'s'
|| *shape++ is not (byte)'h'
|| *shape++ is not (byte)'p'
|| *shape++ is not (byte)'x'
|| *shape++ is not (byte)'_'
|| *shape is 0)
return false;
return true;
}
public bool ValidateCustomShapeString()
{
// "shpx_*"
if (Length < 6)
return false;
if (_buffer[0] is not (byte)'s'
|| _buffer[1] is not (byte)'h'
|| _buffer[2] is not (byte)'p'
|| _buffer[3] is not (byte)'x'
|| _buffer[4] is not (byte)'_')
return false;
return true;
}
public static unsafe bool ValidateCustomAttributeString(byte* shape)
{
// "atrx_*"
if (shape is null)
return false;
if (*shape++ is not (byte)'a'
|| *shape++ is not (byte)'t'
|| *shape++ is not (byte)'r'
|| *shape++ is not (byte)'x'
|| *shape++ is not (byte)'_'
|| *shape is 0)
return false;
return true;
}
public bool ValidateCustomAttributeString()
{
// "atrx_*"
if (Length < 6)
return false;
if (_buffer[0] is not (byte)'a'
|| _buffer[1] is not (byte)'t'
|| _buffer[2] is not (byte)'r'
|| _buffer[3] is not (byte)'x'
|| _buffer[4] is not (byte)'_')
return false;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsAnkle() public bool IsAnkle()
=> CheckCenter('a', 'n'); => CheckCenter('a', 'n');
@ -53,28 +119,28 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
private bool CheckCenter(char first, char second) private bool CheckCenter(char first, char second)
=> Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_'; => Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_';
public bool Equals(ShapeString other) public bool Equals(ShapeAttributeString other)
=> Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]);
public override bool Equals(object? obj) public override bool Equals(object? obj)
=> obj is ShapeString other && Equals(other); => obj is ShapeAttributeString other && Equals(other);
public override int GetHashCode() public override int GetHashCode()
=> (int)Crc32.Get(_buffer[..Length]); => (int)Crc32.Get(_buffer[..Length]);
public static bool operator ==(ShapeString left, ShapeString right) public static bool operator ==(ShapeAttributeString left, ShapeAttributeString right)
=> left.Equals(right); => left.Equals(right);
public static bool operator !=(ShapeString left, ShapeString right) public static bool operator !=(ShapeAttributeString left, ShapeAttributeString right)
=> !left.Equals(right); => !left.Equals(right);
public static unsafe bool TryRead(byte* pointer, out ShapeString ret) public static unsafe bool TryRead(byte* pointer, out ShapeAttributeString ret)
{ {
var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer); var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer);
return TryRead(span, out ret); return TryRead(span, out ret);
} }
public unsafe int CompareTo(ShapeString other) public unsafe int CompareTo(ShapeAttributeString other)
{ {
fixed (void* lhs = &this) fixed (void* lhs = &this)
{ {
@ -82,7 +148,7 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
} }
} }
public static bool TryRead(ReadOnlySpan<byte> utf8, out ShapeString ret) public static bool TryRead(ReadOnlySpan<byte> utf8, out ShapeAttributeString ret)
{ {
if (utf8.Length is 0 or > MaxLength) if (utf8.Length is 0 or > MaxLength)
{ {
@ -97,7 +163,7 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
return true; return true;
} }
public static bool TryRead(ReadOnlySpan<char> utf16, out ShapeString ret) public static bool TryRead(ReadOnlySpan<char> utf16, out ShapeAttributeString ret)
{ {
ret = Empty; ret = Empty;
if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written)) if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written))
@ -116,19 +182,20 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
_buffer[31] = length; _buffer[31] = length;
} }
private sealed class Converter : JsonConverter<ShapeString> private sealed class Converter : JsonConverter<ShapeAttributeString>
{ {
public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, ShapeAttributeString value, JsonSerializer serializer)
{ {
writer.WriteValue(value.ToString()); writer.WriteValue(value.ToString());
} }
public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue, public override ShapeAttributeString ReadJson(JsonReader reader, Type objectType, ShapeAttributeString existingValue,
bool hasExistingValue,
JsonSerializer serializer) JsonSerializer serializer)
{ {
var value = serializer.Deserialize<string>(reader); var value = serializer.Deserialize<string>(reader);
if (!TryRead(value, out existingValue)) if (!TryRead(value, out existingValue))
throw new JsonReaderException($"Could not parse {value} into ShapeString."); throw new JsonReaderException($"Could not parse {value} into ShapeAttributeString.");
return existingValue; return existingValue;
} }

View file

@ -1,144 +0,0 @@
using System.Reflection.Metadata.Ecma335;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta;
public class ShapeManager : IRequiredService, IDisposable
{
public const int NumSlots = 14;
public const int ModelSlotSize = 18;
private readonly AttributeHook _attributeHook;
public static ReadOnlySpan<HumanSlot> UsedModels
=>
[
HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists,
HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear,
];
public ShapeManager(AttributeHook attributeHook)
{
_attributeHook = attributeHook;
_attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager);
}
private readonly Dictionary<ShapeString, short>[] _temporaryIndices =
Enumerable.Range(0, NumSlots).Select(_ => new Dictionary<ShapeString, short>()).ToArray();
private readonly uint[] _temporaryMasks = new uint[NumSlots];
private readonly uint[] _temporaryValues = new uint[NumSlots];
private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize];
public void Dispose()
=> _attributeHook.Unsubscribe(OnAttributeComputed);
private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
{
if (!collection.HasCache)
return;
ComputeCache(model, collection.MetaCache!.Shp);
for (var i = 0; i < NumSlots; ++i)
{
if (_temporaryMasks[i] is 0)
continue;
var modelIndex = UsedModels[i];
var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask;
var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i];
Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}.");
model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask;
}
}
private unsafe void ComputeCache(Model human, ShpCache cache)
{
for (var i = 0; i < NumSlots; ++i)
{
_temporaryMasks[i] = 0;
_temporaryValues[i] = 0;
_temporaryIndices[i].Clear();
var modelIndex = UsedModels[i];
var model = human.AsHuman->Models[modelIndex.ToIndex()];
if (model is null || model->ModelResourceHandle is null)
continue;
_ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set;
ref var shapes = ref model->ModelResourceHandle->Shapes;
foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value)))
{
if (ShapeString.TryRead(shape.Value, out var shapeString))
{
_temporaryIndices[i].TryAdd(shapeString, index);
_temporaryMasks[i] |= (ushort)(1 << index);
if (cache.State.Count > 0
&& cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex]))
_temporaryValues[i] |= (ushort)(1 << index);
}
else
{
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
}
}
}
UpdateDefaultMasks(cache);
}
private void UpdateDefaultMasks(ShpCache cache)
{
foreach (var (shape, topIndex) in _temporaryIndices[1])
{
if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex))
{
_temporaryValues[1] |= 1u << topIndex;
_temporaryValues[2] |= 1u << handIndex;
CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2);
}
if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex))
{
_temporaryValues[1] |= 1u << topIndex;
_temporaryValues[3] |= 1u << legIndex;
CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3);
}
}
foreach (var (shape, bottomIndex) in _temporaryIndices[3])
{
if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex))
{
_temporaryValues[3] |= 1u << bottomIndex;
_temporaryValues[4] |= 1u << footIndex;
CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
}
}
return;
void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2)
{
if (!cache.CheckConditionState(shape, out var dict))
return;
foreach (var (subShape, set) in dict)
{
if (set.Contains(slot1, _ids[idx1]))
if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex))
_temporaryValues[idx1] |= 1u << subIndex;
if (set.Contains(slot2, _ids[idx2]))
if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex))
_temporaryValues[idx2] |= 1u << subIndex;
}
}
}
}

View file

@ -6,6 +6,7 @@ using OtterGui.Extensions;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
@ -44,13 +45,13 @@ public class ModMerger : IDisposable, IService
public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates, public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates,
CommunicatorService communicator, ModCreator creator, Configuration config) CommunicatorService communicator, ModCreator creator, Configuration config)
{ {
_editor = editor; _editor = editor;
_selection = selection; _selection = selection;
_duplicates = duplicates; _duplicates = duplicates;
_communicator = communicator; _communicator = communicator;
_creator = creator; _creator = creator;
_config = config; _config = config;
_mods = mods; _mods = mods;
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger); _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger);
} }
@ -99,26 +100,117 @@ public class ModMerger : IDisposable, IService
foreach (var originalGroup in MergeFromMod!.Groups) foreach (var originalGroup in MergeFromMod!.Groups)
{ {
var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); switch (originalGroup.Type)
if (groupCreated)
_createdGroups.Add(groupIdx);
if (group == null)
throw new Exception(
$"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}.");
foreach (var originalOption in originalGroup.DataContainers)
{ {
var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); case GroupType.Single:
if (optionCreated) case GroupType.Multi:
{ {
_createdOptions.Add(option!); var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name);
// #TODO DataContainer <> Option. if (group is null)
MergeIntoOption([originalOption], (IModDataContainer)option!, false); throw new Exception(
$"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}.");
if (groupCreated)
{
_createdGroups.Add(groupIdx);
group.Description = originalGroup.Description;
group.Image = originalGroup.Image;
group.DefaultSettings = originalGroup.DefaultSettings;
group.Page = originalGroup.Page;
group.Priority = originalGroup.Priority;
}
foreach (var originalOption in originalGroup.Options)
{
var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.Name);
if (optionCreated)
{
_createdOptions.Add(option!);
MergeIntoOption([(IModDataContainer)originalOption], (IModDataContainer)option!, false);
option!.Description = originalOption.Description;
if (option is MultiSubMod multiOption)
multiOption.Priority = ((MultiSubMod)originalOption).Priority;
}
else
{
throw new Exception(
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed.");
}
}
break;
} }
else
case GroupType.Imc when originalGroup is ImcModGroup imc:
{ {
throw new Exception( var group = _editor.ImcEditor.AddModGroup(MergeToMod!, imc.Name, imc.Identifier, imc.DefaultEntry);
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); if (group is null)
throw new Exception(
$"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged.");
group.AllVariants = imc.AllVariants;
group.OnlyAttributes = imc.OnlyAttributes;
group.Description = imc.Description;
group.Image = imc.Image;
group.DefaultSettings = imc.DefaultSettings;
group.Page = imc.Page;
group.Priority = imc.Priority;
foreach (var originalOption in imc.OptionData)
{
if (originalOption.IsDisableSubMod)
{
_editor.ImcEditor.ChangeCanBeDisabled(group, true);
var disable = group.OptionData.First(s => s.IsDisableSubMod);
disable.Description = originalOption.Description;
disable.Name = originalOption.Name;
continue;
}
var newOption = _editor.ImcEditor.AddOption(group, originalOption.Name);
if (newOption is null)
throw new Exception(
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating IMC option {originalOption.FullName}.");
newOption.Description = originalOption.Description;
newOption.AttributeMask = originalOption.AttributeMask;
}
break;
}
case GroupType.Combining when originalGroup is CombiningModGroup combining:
{
var group = _editor.CombiningEditor.AddModGroup(MergeToMod!, combining.Name);
if (group is null)
throw new Exception(
$"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged.");
group.Description = combining.Description;
group.Image = combining.Image;
group.DefaultSettings = combining.DefaultSettings;
group.Page = combining.Page;
group.Priority = combining.Priority;
foreach (var originalOption in combining.OptionData)
{
var option = _editor.CombiningEditor.AddOption(group, originalOption.Name);
if (option is null)
throw new Exception(
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating combining option {originalOption.FullName}.");
option.Description = originalOption.Description;
}
if (group.Data.Count != combining.Data.Count)
throw new Exception(
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error caused data container counts in combining group {originalGroup.Name} to differ.");
foreach (var (originalContainer, container) in combining.Data.Zip(group.Data))
{
container.Name = originalContainer.Name;
MergeIntoOption([originalContainer], container, false);
}
break;
} }
} }
} }
@ -151,7 +243,6 @@ public class ModMerger : IDisposable, IService
if (!dir.Exists) if (!dir.Exists)
_createdDirectories.Add(dir.FullName); _createdDirectories.Add(dir.FullName);
CopyFiles(dir); CopyFiles(dir);
// #TODO DataContainer <> Option.
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true); MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true);
} }
@ -281,7 +372,6 @@ public class ModMerger : IDisposable, IService
} }
else else
{ {
// TODO DataContainer <> Option.
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName()); var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name); var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);

View file

@ -64,6 +64,8 @@ public class ModMetaEditor(
OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est));
OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp));
OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch)); OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch));
OtherData[MetaManipulationType.Shp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Shp));
OtherData[MetaManipulationType.Atr].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atr));
OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp));
} }

View file

@ -76,7 +76,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
else else
{ {
var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport); var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport);
var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport); var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetDirectoryName(), config.ReplaceNonAsciiOnImport);
containers[container] = optionDir.FullName; containers[container] = optionDir.FullName;
} }
} }
@ -286,7 +286,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict) void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict)
{ {
var name = option.GetName(); var name = option.GetDirectoryName();
var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true); var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true);
newDict.Clear(); newDict.Clear();

View file

@ -0,0 +1,95 @@
using System.Collections.Frozen;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
using OtterGui.Text;
using Penumbra.Mods.Manager;
using Penumbra.UI.Classes;
using Notification = OtterGui.Classes.Notification;
namespace Penumbra.Mods;
public static class FeatureChecker
{
/// <summary> Manually setup supported features to exclude None and Invalid and not make something supported too early. </summary>
private static readonly FrozenDictionary<string, FeatureFlags> SupportedFlags = new[]
{
FeatureFlags.Atch,
FeatureFlags.Shp,
FeatureFlags.Atr,
}.ToFrozenDictionary(f => f.ToString(), f => f);
public static IReadOnlyCollection<string> SupportedFeatures
=> SupportedFlags.Keys;
public static FeatureFlags ParseFlags(string modDirectory, string modName, IEnumerable<string> features)
{
var featureFlags = FeatureFlags.None;
HashSet<string> missingFeatures = [];
foreach (var feature in features)
{
if (SupportedFlags.TryGetValue(feature, out var featureFlag))
featureFlags |= featureFlag;
else
missingFeatures.Add(feature);
}
if (missingFeatures.Count > 0)
{
Penumbra.Messager.AddMessage(new Notification(
$"Please update Penumbra to use the mod {modName}{(modDirectory != modName ? $" at {modDirectory}" : string.Empty)}!\n\nLoading failed because it requires the unsupported feature{(missingFeatures.Count > 1 ? $"s\n\n\t[{string.Join("], [", missingFeatures)}]." : $" [{missingFeatures.First()}].")}",
NotificationType.Warning));
return FeatureFlags.Invalid;
}
return featureFlags;
}
public static bool Supported(string features)
=> SupportedFlags.ContainsKey(features);
public static void DrawFeatureFlagInput(ModDataEditor editor, Mod mod, float width)
{
const int numButtons = 5;
var innerSpacing = ImGui.GetStyle().ItemInnerSpacing;
var size = new Vector2((width - (numButtons - 1) * innerSpacing.X) / numButtons, 0);
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
var textColor = ImGui.GetColorU32(ImGuiCol.TextDisabled);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, innerSpacing)
.Push(ImGuiStyleVar.FrameBorderSize, 0);
using (var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value())
.Push(ImGuiCol.Button, buttonColor)
.Push(ImGuiCol.Text, textColor))
{
foreach (var flag in SupportedFlags.Values)
{
if (mod.RequiredFeatures.HasFlag(flag))
{
style.Push(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale);
color.Pop(2);
if (ImUtf8.Button($"{flag}", size))
editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures & ~flag);
color.Push(ImGuiCol.Button, buttonColor)
.Push(ImGuiCol.Text, textColor);
style.Pop();
}
else if (ImUtf8.Button($"{flag}", size))
{
editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures | flag);
}
ImGui.SameLine();
}
}
if (ImUtf8.ButtonEx("Compute"u8, "Compute the required features automatically from the used features."u8, size))
editor.ChangeRequiredFeatures(mod, mod.ComputeRequiredFeatures());
ImGui.SameLine();
if (ImUtf8.ButtonEx("Clear"u8, "Clear all required features."u8, size))
editor.ChangeRequiredFeatures(mod, FeatureFlags.None);
ImGui.SameLine();
ImUtf8.Text("Required Features"u8);
}
}

View file

@ -0,0 +1,180 @@
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using OtterGui.Extensions;
using Penumbra.Api.Enums;
using Penumbra.GameData.Data;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
using Penumbra.UI.ModsTab.Groups;
using Penumbra.Util;
namespace Penumbra.Mods.Groups;
public sealed class ComplexModGroup(Mod mod) : IModGroup
{
public Mod Mod { get; } = mod;
public string Name { get; set; } = "Option";
public string Description { get; set; } = string.Empty;
public string Image { get; set; } = string.Empty;
public GroupType Type
=> GroupType.Complex;
public GroupDrawBehaviour Behaviour
=> GroupDrawBehaviour.Complex;
public ModPriority Priority { get; set; }
public int Page { get; set; }
public Setting DefaultSettings { get; set; }
public readonly List<ComplexSubMod> Options = [];
public readonly List<ComplexDataContainer> Containers = [];
public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> throw new NotImplementedException();
public IModOption? AddOption(string name, string description = "")
=> throw new NotImplementedException();
IReadOnlyList<IModOption> IModGroup.Options
=> Options;
IReadOnlyList<IModDataContainer> IModGroup.DataContainers
=> Containers;
public bool IsOption
=> Options.Count > 0;
public int GetIndex()
=> ModGroup.GetIndex(this);
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer)
=> throw new NotImplementedException();
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
{
foreach (var container in Containers.Where(c => c.Association.IsEnabled(setting)))
SubMod.AddContainerTo(container, redirections, manipulations);
}
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{
foreach (var container in Containers)
identifier.AddChangedItems(container, changedItems);
}
public Setting FixSetting(Setting setting)
=> new(setting.Value & ((1ul << Options.Count) - 1));
public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null)
{
ModSaveGroup.WriteJsonBase(jWriter, this);
jWriter.WritePropertyName("Options");
jWriter.WriteStartArray();
foreach (var option in Options)
{
jWriter.WriteStartObject();
SubMod.WriteModOption(jWriter, option);
if (!option.Conditions.IsZero)
{
jWriter.WritePropertyName("ConditionMask");
jWriter.WriteValue(option.Conditions.Mask.Value);
jWriter.WritePropertyName("ConditionValue");
jWriter.WriteValue(option.Conditions.Value.Value);
}
if (option.Indentation > 0)
{
jWriter.WritePropertyName("Indentation");
jWriter.WriteValue(option.Indentation);
}
if (option.SubGroupLabel.Length > 0)
{
jWriter.WritePropertyName("SubGroup");
jWriter.WriteValue(option.SubGroupLabel);
}
jWriter.WriteEndObject();
}
jWriter.WriteEndArray();
jWriter.WritePropertyName("Containers");
jWriter.WriteStartArray();
foreach (var container in Containers)
{
jWriter.WriteStartObject();
if (container.Name.Length > 0)
{
jWriter.WritePropertyName("Name");
jWriter.WriteValue(container.Name);
}
if (!container.Association.IsZero)
{
jWriter.WritePropertyName("AssociationMask");
jWriter.WriteValue(container.Association.Mask.Value);
jWriter.WritePropertyName("AssociationValue");
jWriter.WriteValue(container.Association.Value.Value);
}
SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath);
jWriter.WriteEndObject();
}
jWriter.WriteEndArray();
}
public (int Redirections, int Swaps, int Manips) GetCounts()
=> ModGroup.GetCountsBase(this);
public static ComplexModGroup? Load(Mod mod, JObject json)
{
var ret = new ComplexModGroup(mod);
if (!ModSaveGroup.ReadJsonBase(json, ret))
return null;
var options = json["Options"];
if (options != null)
foreach (var child in options.Children())
{
if (ret.Options.Count == IModGroup.MaxComplexOptions)
{
Penumbra.Messager.NotificationMessage(
$"Complex Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxComplexOptions} options, ignoring excessive options.",
NotificationType.Warning);
break;
}
var subMod = new ComplexSubMod(ret, child);
ret.Options.Add(subMod);
}
// Fix up conditions: No condition on itself.
foreach (var (option, index) in ret.Options.WithIndex())
{
option.Conditions = option.Conditions.Limit(ret.Options.Count);
option.Conditions = new MaskedSetting(option.Conditions.Mask.SetBit(index, false), option.Conditions.Value);
}
var containers = json["Containers"];
if (containers != null)
foreach (var child in containers.Children())
{
var container = new ComplexDataContainer(ret, child);
container.Association = container.Association.Limit(ret.Options.Count);
ret.Containers.Add(container);
}
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
return ret;
}
}

View file

@ -18,11 +18,13 @@ public enum GroupDrawBehaviour
{ {
SingleSelection, SingleSelection,
MultiSelection, MultiSelection,
Complex,
} }
public interface IModGroup public interface IModGroup
{ {
public const int MaxMultiOptions = 32; public const int MaxMultiOptions = 32;
public const int MaxComplexOptions = MaxMultiOptions;
public const int MaxCombiningOptions = 8; public const int MaxCombiningOptions = 8;
public Mod Mod { get; } public Mod Mod { get; }

View file

@ -234,9 +234,56 @@ public static class EquipmentSwap
mdl.ChildSwaps.Add(mtrl); mdl.ChildSwaps.Add(mtrl);
} }
FixAttributes(mdl, slotFrom, slotTo);
return mdl; return mdl;
} }
private static void FixAttributes(FileSwap swap, EquipSlot slotFrom, EquipSlot slotTo)
{
if (slotFrom == slotTo)
return;
var needle = slotTo switch
{
EquipSlot.Head => "atr_mv_",
EquipSlot.Ears => "atr_ev_",
EquipSlot.Neck => "atr_nv_",
EquipSlot.Wrists => "atr_wv_",
EquipSlot.RFinger or EquipSlot.LFinger => "atr_rv_",
_ => string.Empty,
};
var replacement = slotFrom switch
{
EquipSlot.Head => 'm',
EquipSlot.Ears => 'e',
EquipSlot.Neck => 'n',
EquipSlot.Wrists => 'w',
EquipSlot.RFinger or EquipSlot.LFinger => 'r',
_ => 'm',
};
var attributes = swap.AsMdl()!.Attributes;
for (var i = 0; i < attributes.Length; ++i)
{
if (FixAttribute(ref attributes[i], needle, replacement))
swap.DataWasChanged = true;
}
}
private static unsafe bool FixAttribute(ref string attribute, string from, char to)
{
if (!attribute.StartsWith(from) || attribute.Length != from.Length + 1 || attribute[^1] is < 'a' or > 'j')
return false;
Span<char> stack = stackalloc char[attribute.Length];
attribute.CopyTo(stack);
stack[4] = to;
attribute = new string(stack);
return true;
}
private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant) private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant)
{ {
slot = i.Type.ToSlot(); slot = i.Type.ToSlot();
@ -399,7 +446,7 @@ public static class EquipmentSwap
return null; return null;
var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo); var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo);
var pathTo = $"{folderTo}{fileName}"; var pathTo = $"{folderTo}{fileName}";
var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo); var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo);
var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom); var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom);

View file

@ -26,6 +26,7 @@ public enum ModDataChangeType : ushort
Image = 0x1000, Image = 0x1000,
DefaultChangedItems = 0x2000, DefaultChangedItems = 0x2000,
PreferredChangedItems = 0x4000, PreferredChangedItems = 0x4000,
RequiredFeatures = 0x8000,
} }
public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService
@ -35,7 +36,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
/// <summary> Create the file containing the meta information about a mod from scratch. </summary> /// <summary> Create the file containing the meta information about a mod from scratch. </summary>
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
string? website) string? website, params string[] tags)
{ {
var mod = new Mod(directory); var mod = new Mod(directory);
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name); mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name);
@ -43,6 +44,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
mod.Description = description ?? mod.Description; mod.Description = description ?? mod.Description;
mod.Version = version ?? mod.Version; mod.Version = version ?? mod.Version;
mod.Website = website ?? mod.Website; mod.Website = website ?? mod.Website;
mod.ModTags = tags;
saveService.ImmediateSaveSync(new ModMeta(mod)); saveService.ImmediateSaveSync(new ModMeta(mod));
} }
@ -95,6 +97,16 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
mod.Website = newWebsite; mod.Website = newWebsite;
saveService.QueueSave(new ModMeta(mod)); saveService.QueueSave(new ModMeta(mod));
communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
}
public void ChangeRequiredFeatures(Mod mod, FeatureFlags flags)
{
if (mod.RequiredFeatures == flags)
return;
mod.RequiredFeatures = flags;
saveService.QueueSave(new ModMeta(mod));
communicatorService.ModDataChanged.Invoke(ModDataChangeType.RequiredFeatures, mod, null);
} }
public void ChangeModTag(Mod mod, int tagIdx, string newTag) public void ChangeModTag(Mod mod, int tagIdx, string newTag)

View file

@ -37,11 +37,11 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
public struct ImportDate : ISortMode<Mod> public struct ImportDate : ISortMode<Mod>
{ {
public string Name public ReadOnlySpan<byte> Name
=> "Import Date (Older First)"; => "Import Date (Older First)"u8;
public string Description public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."; => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
public IEnumerable<IPath> GetChildren(Folder f) public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate)); => f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate));
@ -49,11 +49,11 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
public struct InverseImportDate : ISortMode<Mod> public struct InverseImportDate : ISortMode<Mod>
{ {
public string Name public ReadOnlySpan<byte> Name
=> "Import Date (Newer First)"; => "Import Date (Newer First)"u8;
public string Description public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."; => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
public IEnumerable<IPath> GetChildren(Folder f) public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate)); => f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate));
@ -80,7 +80,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
// Update sort order when defaulted mod names change. // Update sort order when defaulted mod names change.
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
{ {
if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !FindLeaf(mod, out var leaf)) if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !TryGetValue(mod, out var leaf))
return; return;
var old = oldName.FixName(); var old = oldName.FixName();
@ -111,7 +111,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
CreateDuplicateLeaf(parent, mod.Name.Text, mod); CreateDuplicateLeaf(parent, mod.Name.Text, mod);
break; break;
case ModPathChangeType.Deleted: case ModPathChangeType.Deleted:
if (FindLeaf(mod, out var leaf)) if (TryGetValue(mod, out var leaf))
Delete(leaf); Delete(leaf);
break; break;
@ -124,16 +124,6 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
} }
} }
// Search the entire filesystem for the leaf corresponding to a mod.
public bool FindLeaf(Mod mod, [NotNullWhen(true)] out Leaf? leaf)
{
leaf = Root.GetAllDescendants(ISortMode<Mod>.Lexicographical)
.OfType<Leaf>()
.FirstOrDefault(l => l.Value == mod);
return leaf != null;
}
// Used for saving and loading. // Used for saving and loading.
private static string ModToIdentifier(Mod mod) private static string ModToIdentifier(Mod mod)
=> mod.ModPath.Name; => mod.ModPath.Name;

View file

@ -70,7 +70,6 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd
_import = null; _import = null;
} }
public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod) public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod)
{ {
if (!_modsToAdd.TryDequeue(out var directory)) if (!_modsToAdd.TryDequeue(out var directory))

View file

@ -1,5 +1,6 @@
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Interop;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Services; using Penumbra.Services;
@ -143,9 +144,10 @@ public sealed class ModManager : ModStorage, IDisposable, IService
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!Creator.ReloadMod(mod, true, false, out var metaChange)) if (!Creator.ReloadMod(mod, true, false, out var metaChange))
{ {
Penumbra.Log.Warning(mod.Name.Length == 0 if (mod.RequiredFeatures is not FeatureFlags.Invalid)
? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." Penumbra.Log.Warning(mod.Name.Length == 0
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead."); ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead."
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead.");
RemoveMod(mod); RemoveMod(mod);
return; return;
} }
@ -251,12 +253,8 @@ public sealed class ModManager : ModStorage, IDisposable, IService
{ {
switch (type) switch (type)
{ {
case ModPathChangeType.Added: case ModPathChangeType.Added: SetNew(mod); break;
SetNew(mod); case ModPathChangeType.Deleted: SetKnown(mod); break;
break;
case ModPathChangeType.Deleted:
SetKnown(mod);
break;
case ModPathChangeType.Moved: case ModPathChangeType.Moved:
if (oldDirectory != null && newDirectory != null) if (oldDirectory != null && newDirectory != null)
DataEditor.MoveDataFile(oldDirectory, newDirectory); DataEditor.MoveDataFile(oldDirectory, newDirectory);
@ -306,6 +304,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService
if (!firstTime && _config.ModDirectory != BasePath.FullName) if (!firstTime && _config.ModDirectory != BasePath.FullName)
TriggerModDirectoryChange(BasePath.FullName, Valid); TriggerModDirectoryChange(BasePath.FullName, Valid);
} }
if (CloudApi.IsCloudSynced(BasePath.FullName))
Penumbra.Log.Warning($"Mod base directory {BasePath.FullName} is cloud-synced. This may cause issues.");
} }
private void TriggerModDirectoryChange(string newPath, bool valid) private void TriggerModDirectoryChange(string newPath, bool valid)

View file

@ -15,8 +15,8 @@ public abstract class ModOptionEditor<TGroup, TOption>(
where TOption : class, IModOption where TOption : class, IModOption
{ {
protected readonly CommunicatorService Communicator = communicator; protected readonly CommunicatorService Communicator = communicator;
protected readonly SaveService SaveService = saveService; protected readonly SaveService SaveService = saveService;
protected readonly Configuration Config = config; protected readonly Configuration Config = config;
/// <summary> Add a new, empty option group of the given type and name. </summary> /// <summary> Add a new, empty option group of the given type and name. </summary>
public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync)
@ -25,7 +25,7 @@ public abstract class ModOptionEditor<TGroup, TOption>(
return null; return null;
var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1;
var group = CreateGroup(mod, newName, maxPriority); var group = CreateGroup(mod, newName, maxPriority);
mod.Groups.Add(group); mod.Groups.Add(group);
SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1);
@ -92,8 +92,8 @@ public abstract class ModOptionEditor<TGroup, TOption>(
/// <summary> Delete the given option from the given group. </summary> /// <summary> Delete the given option from the given group. </summary>
public void DeleteOption(TOption option) public void DeleteOption(TOption option)
{ {
var mod = option.Mod; var mod = option.Mod;
var group = option.Group; var group = option.Group;
var optionIdx = option.GetIndex(); var optionIdx = option.GetIndex();
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1);
RemoveOption((TGroup)group, optionIdx); RemoveOption((TGroup)group, optionIdx);
@ -104,7 +104,7 @@ public abstract class ModOptionEditor<TGroup, TOption>(
/// <summary> Move an option inside the given option group. </summary> /// <summary> Move an option inside the given option group. </summary>
public void MoveOption(TOption option, int optionIdxTo) public void MoveOption(TOption option, int optionIdxTo)
{ {
var idx = option.GetIndex(); var idx = option.GetIndex();
var group = (TGroup)option.Group; var group = (TGroup)option.Group;
if (!MoveOption(group, idx, optionIdxTo)) if (!MoveOption(group, idx, optionIdxTo))
return; return;
@ -113,10 +113,10 @@ public abstract class ModOptionEditor<TGroup, TOption>(
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx);
} }
protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync);
protected abstract TOption? CloneOption(TGroup group, IModOption option); protected abstract TOption? CloneOption(TGroup group, IModOption option);
protected abstract void RemoveOption(TGroup group, int optionIndex); protected abstract void RemoveOption(TGroup group, int optionIndex);
protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo);
} }
public static class ModOptionChangeTypeExtension public static class ModOptionChangeTypeExtension
@ -132,22 +132,22 @@ public static class ModOptionChangeTypeExtension
{ {
(requiresSaving, requiresReloading, wasPrepared) = type switch (requiresSaving, requiresReloading, wasPrepared) = type switch
{ {
ModOptionChangeType.GroupRenamed => (true, false, false), ModOptionChangeType.GroupRenamed => (true, false, false),
ModOptionChangeType.GroupAdded => (true, false, false), ModOptionChangeType.GroupAdded => (true, false, false),
ModOptionChangeType.GroupDeleted => (true, true, false), ModOptionChangeType.GroupDeleted => (true, true, false),
ModOptionChangeType.GroupMoved => (true, false, false), ModOptionChangeType.GroupMoved => (true, false, false),
ModOptionChangeType.GroupTypeChanged => (true, true, true), ModOptionChangeType.GroupTypeChanged => (true, true, true),
ModOptionChangeType.PriorityChanged => (true, true, true), ModOptionChangeType.PriorityChanged => (true, true, true),
ModOptionChangeType.OptionAdded => (true, true, true), ModOptionChangeType.OptionAdded => (true, true, true),
ModOptionChangeType.OptionDeleted => (true, true, false), ModOptionChangeType.OptionDeleted => (true, true, false),
ModOptionChangeType.OptionMoved => (true, false, false), ModOptionChangeType.OptionMoved => (true, false, false),
ModOptionChangeType.OptionFilesChanged => (false, true, false), ModOptionChangeType.OptionFilesChanged => (false, true, false),
ModOptionChangeType.OptionFilesAdded => (false, true, true), ModOptionChangeType.OptionFilesAdded => (false, true, true),
ModOptionChangeType.OptionSwapsChanged => (false, true, false), ModOptionChangeType.OptionSwapsChanged => (false, true, false),
ModOptionChangeType.OptionMetaChanged => (false, true, false), ModOptionChangeType.OptionMetaChanged => (false, true, false),
ModOptionChangeType.DisplayChange => (false, false, false), ModOptionChangeType.DisplayChange => (false, false, false),
ModOptionChangeType.DefaultOptionChanged => (true, false, false), ModOptionChangeType.DefaultOptionChanged => (true, false, false),
_ => (false, false, false), _ => (false, false, false),
}; };
} }
} }

View file

@ -1,4 +1,3 @@
using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Extensions; using OtterGui.Extensions;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
@ -12,6 +11,16 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods; namespace Penumbra.Mods;
[Flags]
public enum FeatureFlags : ulong
{
None = 0,
Atch = 1ul << 0,
Shp = 1ul << 1,
Atr = 1ul << 2,
Invalid = 1ul << 62,
}
public sealed class Mod : IMod public sealed class Mod : IMod
{ {
public static readonly TemporaryMod ForcedFiles = new() public static readonly TemporaryMod ForcedFiles = new()
@ -57,6 +66,7 @@ public sealed class Mod : IMod
public string Image { get; internal set; } = string.Empty; public string Image { get; internal set; } = string.Empty;
public IReadOnlyList<string> ModTags { get; internal set; } = []; public IReadOnlyList<string> ModTags { get; internal set; } = [];
public HashSet<CustomItemId> DefaultPreferredItems { get; internal set; } = []; public HashSet<CustomItemId> DefaultPreferredItems { get; internal set; } = [];
public FeatureFlags RequiredFeatures { get; internal set; } = 0;
// Local Data // Local Data
@ -70,6 +80,23 @@ public sealed class Mod : IMod
public readonly DefaultSubMod Default; public readonly DefaultSubMod Default;
public readonly List<IModGroup> Groups = []; public readonly List<IModGroup> Groups = [];
/// <summary> Compute the required feature flags for this mod. </summary>
public FeatureFlags ComputeRequiredFeatures()
{
var flags = FeatureFlags.None;
foreach (var option in AllDataContainers)
{
if (option.Manipulations.Atch.Count > 0)
flags |= FeatureFlags.Atch;
if (option.Manipulations.Atr.Count > 0)
flags |= FeatureFlags.Atr;
if (option.Manipulations.Shp.Count > 0)
flags |= FeatureFlags.Shp;
}
return flags;
}
public AppliedModData GetData(ModSettings? settings = null) public AppliedModData GetData(ModSettings? settings = null)
{ {
if (settings is not { Enabled: true }) if (settings is not { Enabled: true })

View file

@ -28,15 +28,16 @@ public partial class ModCreator(
MetaFileManager metaFileManager, MetaFileManager metaFileManager,
GamePathParser gamePathParser) : IService GamePathParser gamePathParser) : IService
{ {
public readonly Configuration Config = config; public const FeatureFlags SupportedFeatures = FeatureFlags.Atch | FeatureFlags.Shp | FeatureFlags.Atr;
public readonly Configuration Config = config;
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary> /// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null) public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null, params string[] tags)
{ {
try try
{ {
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty); dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty, tags);
CreateDefaultFiles(newDir); CreateDefaultFiles(newDir);
return newDir; return newDir;
} }
@ -74,7 +75,7 @@ public partial class ModCreator(
return false; return false;
modDataChange = ModMeta.Load(dataEditor, this, mod); modDataChange = ModMeta.Load(dataEditor, this, mod);
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0 || mod.RequiredFeatures is FeatureFlags.Invalid)
return false; return false;
modDataChange |= ModLocalData.Load(dataEditor, mod); modDataChange |= ModLocalData.Load(dataEditor, mod);
@ -82,9 +83,9 @@ public partial class ModCreator(
LoadAllGroups(mod); LoadAllGroups(mod);
if (incorporateMetaChanges) if (incorporateMetaChanges)
IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges); IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges);
else if (deleteDefaultMetaChanges) else if (deleteDefaultMetaChanges)
ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false); ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false);
return true; return true;
} }

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