Compare commits

...

248 commits

Author SHA1 Message Date
Karou
ccb5b01290 Api version bump and remove redundant framework thread call
Some checks failed
.NET Build / build (push) Has been cancelled
2025-12-05 13:39:19 +01:00
Actions User
5dd74297c6 [CI] Updating repo.json for 1.5.1.8
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-28 22:10:17 +00:00
Karou
ce54aa5d25 Added IPC call to allow for redrawing only members of specified collections
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-03 15:15:40 +01:00
Actions User
c4b6e4e00b [CI] Updating repo.json for testing_1.5.1.7
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-23 21:50:20 +00:00
Ottermandias
912c183fc6 Improve file watcher. 2025-10-23 23:45:20 +02:00
Ottermandias
5bf901d0c4 Update actorobjectmanager when setting cutscene index.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-23 17:30:29 +02:00
Ottermandias
cbedc878b9 Slight cleanup and autoformat. 2025-10-22 21:56:16 +02:00
Ottermandias
c8cf560fc1 Merge branch 'refs/heads/StoiaCode/fileWatcher' 2025-10-22 21:48:42 +02:00
Stoia
f05cb52da2 Add Option to notify instead of auto install.
And General Fixes
2025-10-22 18:20:44 +02:00
Ottermandias
7ed81a9823 Update OtterGui.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-22 17:53:02 +02:00
Stoia
60aa23efcd
Merge branch 'xivdev:master' into fileWatcher 2025-10-22 14:28:08 +02:00
Ottermandias
ebbe957c95 Remove login screen log spam.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-11 20:13:51 +02:00
Actions User
300e0e6d84 [CI] Updating repo.json for 1.5.1.6
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-07 10:45:04 +00:00
Ottermandias
049baa4fe4 Again. 2025-10-07 12:42:54 +02:00
Ottermandias
0881dfde8a Update signatures. 2025-10-07 12:27:35 +02:00
Actions User
23c0506cb8 [CI] Updating repo.json for testing_1.5.1.5
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-28 10:43:01 +00:00
Ottermandias
699745413e Make priority an int. 2025-09-28 12:40:52 +02:00
Actions User
eb53f04c6b [CI] Updating repo.json for testing_1.5.1.4
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 12:03:35 +00:00
Ottermandias
c6b596169c Add default constructor. 2025-09-27 14:01:21 +02:00
Actions User
a0c3e820b0 [CI] Updating repo.json for testing_1.5.1.3
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 11:02:39 +00:00
Ottermandias
a59689ebfe CS API update and add http API routes. 2025-09-27 13:00:18 +02:00
Exter-N
e9f67a009b Lift "shaders known" restriction for saving materials
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-19 11:18:39 +02:00
Ottermandias
97c8d82b33 Prevent default-named collection from being renamed and always put it at the top of the selector.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-07 10:45:28 +02:00
Stoia
c3b00ff426 Integrate FileWatcher
HEAVY WIP
2025-09-06 14:22:18 +02:00
Actions User
6348c4a639 [CI] Updating repo.json for 1.5.1.2
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-02 14:25:55 +00:00
Ottermandias
5a6e06df3b git is stupid 2025-09-02 16:22:02 +02:00
Ottermandias
f5f6dd3246 Handle some TODOs. 2025-09-02 16:12:01 +02:00
Ottermandias
4e788f7c2b Update sig. 2025-09-02 11:51:59 +02:00
Ottermandias
ad1659caf6 Update libraries. 2025-09-02 11:29:58 +02:00
Ottermandias
18a6ce2a5f Merge branch 'refs/heads/Exter-N/cldapi'
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-01 15:59:26 +02:00
Ottermandias
e68e821b2a Merge branch 'master' into Exter-N/cldapi 2025-09-01 15:58:22 +02:00
Ottermandias
96764b34ca Merge branch 'refs/heads/Exter-N/restree-stuff' 2025-09-01 15:57:06 +02:00
Exter-N
2cf60b78cd Reject and warn about cloud-synced base directories 2025-08-31 06:42:45 +02:00
Exter-N
d59be1e660 Refine IsCloudSynced 2025-08-31 05:25:37 +02:00
Exter-N
5503bb32e0 CloudApi testing in Debug tab 2025-08-31 04:13:56 +02:00
Exter-N
f3ec4b2e08 Only display the file name and last dir for externals 2025-08-30 19:19:07 +02:00
Exter-N
b3379a9710 Stop redacting external paths 2025-08-30 16:55:20 +02:00
Exter-N
8c25ef4b47 Make the save button ResourceTreeViewer baseline 2025-08-30 16:53:12 +02:00
Ottermandias
912020cc3f Update for staging and wrong tooltip.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-29 16:36:42 +02:00
Ottermandias
be8987a451 Merge branch 'master' of github.com:xivDev/Penumbra
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-28 18:52:29 +02:00
Ottermandias
f7cf5503bb Fix deleting PCP collections. 2025-08-28 18:52:06 +02:00
Ottermandias
a04a5a071c Add warning in file redirections if extension doesn't match. 2025-08-28 18:51:57 +02:00
Actions User
71e24c13c7 [CI] Updating repo.json for 1.5.1.0
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-25 08:39:42 +00:00
Ottermandias
c0120f81af 1.5.1.0 2025-08-25 10:37:38 +02:00
Ottermandias
da47c19aeb Woops, increment version. 2025-08-25 10:25:05 +02:00
Actions User
e16800f216 [CI] Updating repo.json for testing_1.5.0.10 2025-08-25 08:16:04 +00:00
Ottermandias
79a4fc5904 Fix wrong logging. 2025-08-25 10:13:48 +02:00
Ottermandias
bf90725dd2 Fix resolvecontext issue. 2025-08-25 10:13:39 +02:00
Ottermandias
a14347f73a Update temporary collection creation. 2025-08-25 10:13:31 +02:00
Actions User
1e07e43498 [CI] Updating repo.json for testing_1.5.0.9
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-24 13:51:43 +00:00
Ottermandias
f51f8a7bf8 Try to filter meta entries for relevance. 2025-08-24 15:24:57 +02:00
Exter-N
1fca78fa71 Add Kdb files to ResourceTree 2025-08-24 14:09:02 +02:00
Exter-N
c8b6325a87 Add game integrity message to On-Screen 2025-08-24 14:06:39 +02:00
Ottermandias
6079103505 Add collection PCP settings.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-23 14:46:27 +02:00
Actions User
d302a17f1f [CI] Updating repo.json for testing_1.5.0.8
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-22 18:33:43 +00:00
Ottermandias
0d64384059 Add cleanup buttons to PCP, add option to turn off PCP IPC. 2025-08-22 20:31:40 +02:00
Ottermandias
10894d451a Add Pcp Events. 2025-08-22 18:08:22 +02:00
Actions User
fb34238530 [CI] Updating repo.json for testing_1.5.0.7 2025-08-22 13:51:50 +00:00
Ottermandias
8043e6fb6b Add option to disable PCP. 2025-08-22 15:49:15 +02:00
Ottermandias
e3b7f72893 Add initial PCP. 2025-08-22 15:44:33 +02:00
Ottermandias
b7f326e29c Fix bug with collection setting and empty collection. 2025-08-22 15:43:55 +02:00
Ottermandias
dad01e1af8 Update GameData. 2025-08-20 15:24:00 +02:00
Ottermandias
10b71930a1 Merge branch 'refs/heads/Exter-N/stockings-skin-slot' 2025-08-18 15:41:22 +02:00
Ottermandias
23257f94a4 Some cleanup and add option to disable skin material attribute scanning. 2025-08-18 15:41:10 +02:00
Ottermandias
83a36ed4cb Merge branch 'master' into Exter-N/stockings-skin-slot 2025-08-18 15:31:52 +02:00
Ottermandias
8304579d29 Add predefined tags to the multi mod selector. 2025-08-17 13:54:36 +02:00
Actions User
24cbc6c5e1 [CI] Updating repo.json for 1.5.0.6 2025-08-17 08:46:26 +00:00
Exter-N
41edc23820 Allow changing the skin mtrl suffix 2025-08-17 03:11:11 +02:00
Exter-N
aa920b5e9b Fix ImGui texture usage issue 2025-08-17 01:41:49 +02:00
Ottermandias
87ace28bcf Update OtterGui. 2025-08-16 11:56:24 +02:00
Ottermandias
5917f5fad1 Small fixes. 2025-08-13 17:42:45 +02:00
Actions User
f69c264317 [CI] Updating repo.json for 1.5.0.5 2025-08-13 14:53:11 +00:00
Ottermandias
a7246b9d98 Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it. 2025-08-13 16:50:26 +02:00
Actions User
9aff388e21 [CI] Updating repo.json for 1.5.0.4 2025-08-12 12:53:33 +00:00
Ottermandias
091aff1b8a Merge branch 'master' of github.com:xivDev/Penumbra 2025-08-12 14:47:50 +02:00
Ottermandias
9f8185f67b Add new parameter to LoadWeapon hook. 2025-08-12 14:47:35 +02:00
Actions User
b112d75a27 [CI] Updating repo.json for 1.5.0.3 2025-08-12 10:31:13 +00:00
Ottermandias
7af81a6c18 Fix issue with removing default metadata. 2025-08-12 12:29:09 +02:00
Ottermandias
12a218bb2b Protect against empty requested paths. 2025-08-12 12:28:56 +02:00
Ottermandias
f6bac93db7 Update ChangedEquipData. 2025-08-11 19:58:24 +02:00
Actions User
155d3d49aa [CI] Updating repo.json for 1.5.0.2 2025-08-09 16:40:42 +00:00
Exter-N
9aae2210a2 Fix nullptr crashes 2025-08-09 14:57:15 +02:00
Actions User
3785a629ce [CI] Updating repo.json for 1.5.0.1 2025-08-09 11:03:24 +00:00
Ottermandias
02af52671f Need staging again ... 2025-08-09 13:00:40 +02:00
Ottermandias
391c9d727e Fix shifted timeline vfunc offset. 2025-08-09 12:51:39 +02:00
Ottermandias
ff2b2be953 Fix popups not working early. 2025-08-09 12:11:29 +02:00
Ottermandias
6242b30f93 Fix resizable child. 2025-08-09 11:58:35 +02:00
Exter-N
11cd08a9de ClientStructs-ify stuff 2025-08-09 10:29:29 +02:00
Ottermandias
46cfbcb115 Set Repo API level to 13 and remove stg from future releases. 2025-08-08 23:13:23 +02:00
Actions User
66543cc671 [CI] Updating repo.json for 1.5.0.0 2025-08-08 21:12:00 +00:00
Ottermandias
13283c9690 Fix dumb. 2025-08-08 23:08:26 +02:00
Ottermandias
bedfb22466 Use staging for release. 2025-08-08 23:04:50 +02:00
Ottermandias
13df8b2248 Update gamedata. 2025-08-08 23:02:22 +02:00
Ottermandias
93406e4d4e 1.5.0.0 2025-08-08 16:17:59 +02:00
Ridan Vandenbergh
8140d08557 Add vertex material types for usages of 2 colour attributes 2025-08-08 16:15:19 +02:00
Ridan Vandenbergh
2b36f39848 Fix basecolor texture in material export 2025-08-08 16:12:37 +02:00
Ottermandias
a69811800d Update GameData 2025-08-08 15:56:25 +02:00
Ottermandias
3f18ad50de Initial API13 / 7.3 update. 2025-08-08 00:45:24 +02:00
Ridan Vandenbergh
6689e326ee Material tab: disallow "Enable Transparency" for stockings shader 2025-08-02 00:38:24 +02:00
Passive
bdcab22a55 Cleanup methods to extension class 2025-08-02 00:16:55 +02:00
Passive
f5f4fe7259 Invalid tangent fix example 2025-08-02 00:16:55 +02:00
Sebastina
898963fea5 Allow focusing a specified mod via HTTP API under the mods tab. 2025-08-02 00:16:38 +02:00
Ottermandias
8527bfa29c Fix missing updates for OtterGui. 2025-08-02 00:13:35 +02:00
Ottermandias
baca3cdec2 Update Libs. 2025-08-02 00:08:09 +02:00
Ottermandias
dc93eba34c Add initial complex group things. 2025-08-02 00:06:25 +02:00
Ottermandias
012052daa0 Change behavior for directory names. 2025-08-02 00:06:03 +02:00
Ottermandias
a9546e31ee Update packages. 2025-08-02 00:05:27 +02:00
Actions User
a4a6283e7b [CI] Updating repo.json for testing_1.4.0.6 2025-07-14 15:12:06 +00:00
Ottermandias
00c02fd16e Fix tex file migration for small textures. 2025-07-14 17:09:07 +02:00
Ottermandias
140d150bb4 Fix character sound data. 2025-07-14 17:08:46 +02:00
Actions User
49a6d935f3 [CI] Updating repo.json for testing_1.4.0.5 2025-07-05 20:11:28 +00:00
Ottermandias
692beacc2e Merge remote-tracking branch 'Exter-N/human-skin-materials' 2025-07-05 22:04:48 +02:00
Ottermandias
a953febfba Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models. 2025-07-05 22:03:32 +02:00
Ottermandias
c0aa2e36ea Merge branch 'refs/heads/Exter-N/reslogger-tid' 2025-07-05 22:03:14 +02:00
Exter-N
278bf43b29 ClientStructs-ify ResourceTree stuff 2025-07-05 05:20:24 +02:00
Exter-N
a97d9e4953 Add Human skin material handling 2025-07-05 04:37:37 +02:00
Exter-N
30e3cd1f38 Add OS thread ID info to the Resource Logger 2025-07-04 19:41:31 +02:00
Ottermandias
62e9dc164d Add support button. 2025-06-26 14:49:28 +02:00
Actions User
9fc572ba0c [CI] Updating repo.json for testing_1.4.0.4 2025-06-15 21:47:41 +00:00
Ottermandias
3c20b541ce Make mousewheel-scrolling work for setting combos, also filters. 2025-06-15 23:20:13 +02:00
Ottermandias
1961b03d37 Fix issues with shapes and attributes with ID. 2025-06-15 23:18:46 +02:00
Ottermandias
1f4ec984b3 Use improved filesystem. 2025-06-13 17:27:56 +02:00
Ottermandias
4981b0348f BNPCs. 2025-06-13 17:27:56 +02:00
Ottermandias
a8c05fc6ee Make middle-mouse button handle temporary settings. 2025-06-13 17:27:56 +02:00
Actions User
3d05662384 [CI] Updating repo.json for testing_1.4.0.3 2025-06-08 09:38:30 +00:00
Ottermandias
973814b31b Some more BNPCs. 2025-06-08 11:36:32 +02:00
Ottermandias
a16fd85a7e Handle .tex files with broken mip map offsets on import, also remove unnecessary mipmaps (any after reaching minimum size once). 2025-06-08 11:28:12 +02:00
Ottermandias
4c0e6d2a67 Update Mod Merger for other group types. 2025-06-07 22:10:59 +02:00
Ottermandias
535694e9c8 Update some BNPC Names. 2025-06-07 22:10:17 +02:00
Ottermandias
318a41fe52 Add checking for supported features with the currently new supported features 'Atch', 'Shp' and 'Atr'. 2025-06-03 18:39:54 +02:00
Actions User
98203e4e8a [CI] Updating repo.json for testing_1.4.0.2 2025-06-01 11:06:37 +00:00
Ottermandias
6cba63ac98 Make shape names editable in models. 2025-06-01 13:04:26 +02:00
Ottermandias
b48c4f440a Make attributes and shapes completely toggleable. 2025-06-01 13:04:26 +02:00
Actions User
75f4e66dbf [CI] Updating repo.json for 1.4.0.1 2025-05-30 12:38:32 +00:00
Ottermandias
74bd1cf911 Fix checking the flags for all races and genders for specific IDs in shapes/attributes. 2025-05-30 14:36:33 +02:00
Ottermandias
ff2a9f95c4 Fix Atr and Shp not being transmitted via Mare, add improved compression but don't use it yet. 2025-05-30 14:36:07 +02:00
Ottermandias
9921c3332e Merge branch 'master' of github.com:xivDev/Penumbra 2025-05-30 14:35:24 +02:00
Ottermandias
f2927290f5 Fix exceptions when unsubscribing during event invocation. 2025-05-30 14:35:13 +02:00
Actions User
1551d9b6f3 [CI] Updating repo.json for 1.4.0.0 2025-05-28 11:56:49 +00:00
Ottermandias
5e985f4a84 1.4.0.0 2025-05-28 13:54:42 +02:00
Ottermandias
2c115eda94 Slightly improve error message when importing wrongly named atch files. 2025-05-28 13:54:23 +02:00
Ottermandias
ebe45c6a47 Update Lib. 2025-05-27 11:33:10 +02:00
Ottermandias
82fc334be7 Use dynamis for some pointers. 2025-05-23 15:17:19 +02:00
Actions User
cd56163b1b [CI] Updating repo.json for testing_1.3.6.15 2025-05-23 09:32:11 +00:00
Ottermandias
ccc2c1fd4c Fix missing other option notifications for shp/atr. 2025-05-23 11:30:10 +02:00
Ottermandias
08c9124858 Fix issue with shapes/attributes not checking the groups correctly. 2025-05-23 11:29:52 +02:00
Ottermandias
1bdbfe22c1 Update Libraries. 2025-05-23 10:50:04 +02:00
Actions User
9e7c304556 [CI] Updating repo.json for testing_1.3.6.14 2025-05-22 09:16:22 +00:00
Ottermandias
bc4f88aee9 Fix shape/attribute mask stupidity. 2025-05-22 11:14:17 +02:00
Ottermandias
400d7d0bea Slight improvements. 2025-05-22 11:13:58 +02:00
Ottermandias
ac4c75d3c3 Fix not updating meta count correctly. 2025-05-22 11:13:42 +02:00
Ottermandias
507b0a5aee Slight description update. 2025-05-21 18:07:17 +02:00
Actions User
f5db888bbd [CI] Updating repo.json for testing_1.3.6.13 2025-05-21 13:49:29 +00:00
Ottermandias
d7dee39fab Add attribute handling, rework atr and shape caches. 2025-05-21 15:45:05 +02:00
Ottermandias
3412786282 Optimize used memory by metadictionarys a bit. 2025-05-21 15:45:05 +02:00
Ottermandias
861cbc7759 Add global EQP edits to always hide horns or ears. 2025-05-21 15:45:05 +02:00
Actions User
fefa3852f7 [CI] Updating repo.json for testing_1.3.6.12 2025-05-19 15:17:54 +00:00
Ottermandias
68b68d6ce7 Fix some issues with customization IDs and supported counts. 2025-05-19 17:15:29 +02:00
Ottermandias
47b5895404 Fix issue with temp settings again. 2025-05-18 22:00:17 +02:00
Actions User
e18e4bb0e1 [CI] Updating repo.json for testing_1.3.6.11 2025-05-18 14:02:16 +00:00
Ottermandias
6e4e28fa00 Fix disabling conditional shapes. 2025-05-18 16:00:09 +02:00
Ottermandias
e326e3d809 Update shp conditions. 2025-05-18 15:52:47 +02:00
Ottermandias
fbc4c2d054 Improve option select combo. 2025-05-18 12:54:23 +02:00
Ottermandias
3078c467d0 Fix issue with empty and temporary settings. 2025-05-18 12:54:23 +02:00
Ottermandias
52927ff06b Fix clipping in meta edits. 2025-05-18 12:54:23 +02:00
Actions User
08e8b9d2a4 [CI] Updating repo.json for testing_1.3.6.10 2025-05-15 22:28:36 +00:00
Ottermandias
f1448ed947 Add conditional connector shapes. 2025-05-16 00:25:13 +02:00
Ottermandias
c0dcfdd835 Update shape string format. 2025-05-15 22:23:42 +02:00
Actions User
70295b7a6b [CI] Updating repo.json for testing_1.3.6.9 2025-05-15 15:50:16 +00:00
Ottermandias
480942339f Add draggable mod selector width. 2025-05-15 17:47:32 +02:00
Ottermandias
6ad0b4299a Add shape meta manipulations and rework attribute hook. 2025-05-15 17:46:53 +02:00
Ottermandias
0adec35848 Add initial support for custom shapes. 2025-05-15 00:26:59 +02:00
Ottermandias
0fe4a3671a Improve small issue with redraw service. 2025-05-08 23:46:25 +02:00
Caraxi
363d115be8 Add filter for temporary mods 2025-04-19 23:14:39 +02:00
Ottermandias
7595827d29 Merge branch 'master' of github.com:xivDev/Penumbra 2025-04-19 23:12:02 +02:00
Ottermandias
117724b0ae Update npc names. 2025-04-19 23:11:45 +02:00
Ottermandias
a5d221dc13 Make temporary mode checkbox more visible. 2025-04-18 00:17:07 +02:00
Ottermandias
cbebfe5e99 Fix sizing of mod panel. 2025-04-17 01:06:58 +02:00
Ottermandias
0c768979d4 Don't use DalamudPackager for no reason. 2025-04-17 01:06:22 +02:00
Ottermandias
53ef42adfa Update EST Customization identification. 2025-04-17 01:06:09 +02:00
Ottermandias
0954f50912 Update OtterGui, GameData, Namespaces. 2025-04-17 01:05:56 +02:00
Actions User
5d5fc673b1 [CI] Updating repo.json for 1.3.6.8 2025-04-10 14:42:26 +00:00
Ottermandias
2bd0c89588 Better item sort for item swap selectors. 2025-04-10 16:04:49 +02:00
Ottermandias
f03a139e0e blech 2025-04-10 00:17:23 +02:00
Ottermandias
f9b5a626cf Add some migration stuff. 2025-04-10 00:02:49 +02:00
Ottermandias
dc336569ff Add context to copy the full file path from redirections. 2025-04-10 00:02:36 +02:00
Ottermandias
0ec6a17ac7 Add context to open backup directory. 2025-04-10 00:02:21 +02:00
Ottermandias
129156a1c1 Add some more safety and better IPC for draw object storage. 2025-04-09 15:04:47 +02:00
Ottermandias
33ada1d994 Remove meta-default-value checking from TT imports, move it entirely to mod loads, and keep default-valued entries if other options actually edit the same entry. 2025-04-08 16:56:23 +02:00
Ottermandias
0afcae4504 Run API redraws on framework. 2025-04-05 18:49:30 +02:00
Ottermandias
93e60471de Update for new objectmanager. 2025-04-05 18:49:18 +02:00
Actions User
5437ab477f [CI] Updating repo.json for 1.3.6.7 2025-04-05 12:44:47 +00:00
Ottermandias
3b54485127 Maybe fix AtchCaller crashes. 2025-04-05 14:42:25 +02:00
Ottermandias
c3b2443ab5 Add Incognito modifier. 2025-04-04 22:35:23 +02:00
Actions User
2fdafc5c85 [CI] Updating repo.json for 1.3.6.6 2025-04-02 21:45:19 +00:00
Ottermandias
09c2264de4 Revert overeager BNPC Name update. 2025-04-02 23:41:08 +02:00
Ottermandias
c3be151d40 Maybe fix crash issue in AtchHook1 / issue with kept draw object links. 2025-04-02 23:37:06 +02:00
Exter-N
abb47751c8 Mtrl editor: Disregard obsolete modded ShPks 2025-03-30 20:32:03 +02:00
Exter-N
1d517103b3 Mtrl editor: Fix texture pinning 2025-03-30 20:32:03 +02:00
Actions User
fe5d1bc36e [CI] Updating repo.json for 1.3.6.5 2025-03-30 16:08:59 +00:00
Exter-N
b589103b05 Make resolvedData thread-local 2025-03-30 13:56:32 +02:00
Actions User
cc76125b1c [CI] Updating repo.json for 1.3.6.4 2025-03-29 17:07:46 +00:00
Ottermandias
f3bcc4d554 Update changelog. 2025-03-29 18:05:47 +01:00
Ottermandias
2dd6dd201c Update PAP records. 2025-03-29 18:03:57 +01:00
Exter-N
cb0214ca2f Fix material editor and improve pinning logic 2025-03-29 16:55:44 +01:00
Exter-N
5a5a1487a3 Fix texture naming in Resource Trees 2025-03-29 16:55:44 +01:00
Actions User
de408e4d58 [CI] Updating repo.json for 1.3.6.3 2025-03-28 17:33:26 +00:00
Ottermandias
a1bf26e7e8 Run HTTP redraws on framework thread. 2025-03-28 18:30:26 +01:00
Actions User
3bb7db10fb [CI] Updating repo.json for 1.3.6.2 2025-03-28 16:29:20 +00:00
Ottermandias
8a68a1bff5 Update GameData. 2025-03-28 17:25:03 +01:00
Ottermandias
01e6f58463 Add Launching IPC Event. API 5.8 2025-03-28 16:53:50 +01:00
Actions User
7498bc469f [CI] Updating repo.json for 1.3.6.1 2025-03-28 14:55:12 +00:00
Ottermandias
23ba77c107 Update build step and check for pre 7.2 shps. 2025-03-28 15:52:40 +01:00
Ottermandias
1a1d1c1840 Revert Dalamud staging on release, and update api level. 2025-03-28 14:10:52 +01:00
Actions User
b019da2a8c [CI] Updating repo.json for 1.3.6.0 2025-03-28 13:09:26 +00:00
Ottermandias
60becf0a09 Use staging build for release for now. 2025-03-28 14:06:21 +01:00
Ottermandias
974b215610 1.3.6.0 2025-03-28 13:58:11 +01:00
Ottermandias
8e191ae075 Fix offsets. 2025-03-28 13:33:43 +01:00
Ottermandias
b189ac027b Fix imgui assert. 2025-03-28 02:29:49 +01:00
Ottermandias
6cbc8bd58f Merge remote-tracking branch 'Exter-N/72' 2025-03-28 00:59:14 +01:00
Exter-N
49f077aca0 Fixes for 7.2 (ResourceTree + ShPk 13.1) 2025-03-27 22:32:07 +01:00
Ottermandias
525d1c6bf9 Update GameData. 2025-03-27 18:18:04 +01:00
Ottermandias
124b54ab04 Update GameData. 2025-03-27 16:00:30 +01:00
Ottermandias
b8b2127a5d Update STM and signatures. 2025-03-27 15:53:59 +01:00
Ottermandias
586bd9d0cc Re-add wrong dependencies. 2025-03-27 12:07:45 +01:00
Ottermandias
03bb07a9c0 Update for SDK. 2025-03-27 12:04:38 +01:00
Ottermandias
279a861582 Fix error in parser. 2025-03-16 22:17:37 +01:00
Ottermandias
82a1271281 Add option to import atch files from the mod itself via context. 2025-03-16 15:46:51 +01:00
Ottermandias
26a6cc4735 Fix clipping in changed items panel without grouping. 2025-03-16 15:46:51 +01:00
Ottermandias
61d70f7b4e Fix identification of EST changes. 2025-03-16 15:46:51 +01:00
Ottermandias
0213096c58 Add BodyHideGloveCuffs name to eqp entries. 2025-03-16 15:46:51 +01:00
Actions User
dc47a08988 [CI] Updating repo.json for testing_1.3.5.1 2025-03-13 23:20:54 +00:00
Ottermandias
83574dfeb1 Merge remote-tracking branch 'Exter-N/better-tex' 2025-03-14 00:16:33 +01:00
Ottermandias
87f44d7a88 Some minor parser fixes thanks to Anna. 2025-03-14 00:13:57 +01:00
Ottermandias
cda6a4c420 Make preferred changed item star more noticeable, and make the color configurable. 2025-03-14 00:13:01 +01:00
Exter-N
4093228e61 Improve wording of block compressions (suggested by @Theo-Asterio) 2025-03-12 23:04:57 +01:00
Exter-N
442ae960cf Add encoding support for BC1, BC4 and BC5 2025-03-12 20:03:53 +01:00
Exter-N
e7f7077e96 Simplify passing of the device (suggested by @rootdarkarchon) 2025-03-12 15:18:20 +01:00
Exter-N
e5620e17e0 Improve texture saving 2025-03-12 01:20:36 +01:00
Ottermandias
93b0996794 Add chat command to clear temporary settings. 2025-03-11 18:13:08 +01:00
Actions User
1d70be8060 [CI] Updating repo.json for 1.3.5.0 2025-03-09 22:16:58 +00:00
Ottermandias
eab98ec0e4 1.3.5.0 2025-03-09 14:41:45 +01:00
Ottermandias
861b7b78cd Merge branch 'master' of github.com:xivDev/Penumbra 2025-03-09 13:48:56 +01:00
Ottermandias
6eacc82dcd Update references. 2025-03-09 13:48:41 +01:00
Ottermandias
1afbbfef78 Update NuGet packages. 2025-03-09 13:39:41 +01:00
Ottermandias
7cf0367361 Try moving extracted folders 3 times for unknown issues. 2025-03-09 13:39:25 +01:00
Ottermandias
0b0c92eb09 Some cleanup. 2025-03-02 14:27:40 +01:00
Actions User
34d51b66aa [CI] Updating repo.json for testing_1.3.4.6 2025-03-01 21:37:03 +00:00
286 changed files with 8680 additions and 1688 deletions

View file

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

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770

@ -1 +1 @@
Subproject commit 70f046830cc7cd35b3480b12b7efe94182477fbb
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit a21c146790b370bd58b0f752385ae153f7e769c0
Subproject commit d889f9ef918514a46049725052d378b441915b00

@ -1 +1 @@
Subproject commit 4eb7c118cdac5873afb97cb04719602f061f03b7
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793

View file

@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\build.yml = .github\workflows\build.yml
Penumbra\Penumbra.json = Penumbra\Penumbra.json
.github\workflows\release.yml = .github\workflows\release.yml
repo.json = repo.json
.github\workflows\test_release.yml = .github\workflows\test_release.yml
@ -41,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
schemas\structs\group_single.json = schemas\structs\group_single.json
schemas\structs\manipulation.json = schemas\structs\manipulation.json
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
@ -49,39 +51,40 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
schemas\structs\meta_shp.json = schemas\structs\meta_shp.json
schemas\structs\option.json = schemas\structs\option.json
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly CutsceneService _cutsceneService;
private readonly ResourceLoader _resourceLoader;
public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
ResourceLoader resourceLoader)
ResourceLoader resourceLoader, DrawObjectState drawObjectState)
{
_communicator = communicator;
_collectionResolver = collectionResolver;
_cutsceneService = cutsceneService;
_resourceLoader = resourceLoader;
_drawObjectState = drawObjectState;
_resourceLoader.ResourceLoaded += OnResourceLoaded;
_resourceLoader.PapRequested += OnPapRequested;
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
@ -67,6 +69,30 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
public int GetCutsceneParentIndex(int actorIdx)
=> _cutsceneService.GetParentIndex(actorIdx);
public Func<int, int> GetCutsceneParentIndexFunc()
{
var weakRef = new WeakReference<CutsceneService>(_cutsceneService);
return idx =>
{
if (!weakRef.TryGetTarget(out var c))
throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed.");
return c.GetParentIndex(idx);
};
}
public Func<nint, nint> GetGameObjectFromDrawObjectFunc()
{
var weakRef = new WeakReference<DrawObjectState>(_drawObjectState);
return model =>
{
if (!weakRef.TryGetTarget(out var c))
throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed.");
return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero;
};
}
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
? PenumbraApiEc.Success

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.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair<ShpIdentifier, ShpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair<AtrIdentifier, AtrEntry>(kvp.Key, kvp.Value.Entry)));
}
return Functions.ToCompressedBase64(array, 0);
@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
}
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>
/// Convert manipulations from a transmitted base64 string to actual manipulations.
/// 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 1: return ConvertManipsV1(data, out manips);
case 2: return ConvertManipsV2(data, out manips);
default:
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
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)
{
if (!data.StartsWith("META0001"u8))
@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
// Shp and Atr was added later
if (r.Position < r.Count)
{
var shpCount = r.ReadInt32();
for (var i = 0; i < shpCount; ++i)
{
var identifier = r.Read<ShpIdentifier>();
var value = r.Read<ShpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var atrCount = r.ReadInt32();
for (var i = 0; i < atrCount; ++i)
{
var identifier = r.Read<AtrIdentifier>();
var value = r.Read<AtrEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
}
}
return true;

View file

@ -1,4 +1,4 @@
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;

View file

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

View file

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

View file

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

View file

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

View file

@ -20,8 +20,16 @@ public class TemporaryApi(
ApiHelpers apiHelpers,
ModManager modManager) : IPenumbraApiTemporary, IApiService
{
public Guid CreateTemporaryCollection(string name)
=> tempCollections.CreateTemporaryCollection(name);
public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
{
if (!IdentityChecker.Check(identity))
return (PenumbraApiEc.InvalidCredentials, Guid.Empty);
var collection = tempCollections.CreateTemporaryCollection(name);
if (collection == Guid.Empty)
return (PenumbraApiEc.UnknownError, collection);
return (PenumbraApiEc.Success, collection);
}
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
=> tempCollections.RemoveTemporaryCollection(collectionId)

View file

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

View file

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

View file

@ -40,6 +40,8 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState),
IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState),
IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState),
IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta),
IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta),
@ -52,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
IpcSubscribers.ModMoved.Provider(pi, api.Mods),
IpcSubscribers.CreatingPcp.Provider(pi, api.Mods),
IpcSubscribers.ParsingPcp.Provider(pi, api.Mods),
IpcSubscribers.GetModPath.Provider(pi, api.Mods),
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
@ -78,10 +82,13 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Text;
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

@ -1,5 +1,4 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
@ -8,6 +7,7 @@ using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.Util;
using Penumbra.GameData.Data;
using OtterGui.Extensions;
namespace Penumbra.Collections.Cache;
@ -245,6 +245,10 @@ public sealed class CollectionCache : IDisposable
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Atch)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Shp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Atr)
AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!);
}

View file

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

View file

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

View file

@ -16,11 +16,13 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
public readonly RspCache Rsp = new(manager, collection);
public readonly ImcCache Imc = new(manager, collection);
public readonly AtchCache Atch = new(manager, collection);
public readonly ShpCache Shp = new(manager, collection);
public readonly AtrCache Atr = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new();
public bool IsDisposed { get; private set; }
public int Count
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count;
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
@ -30,6 +32,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void Reset()
@ -41,6 +45,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Rsp.Reset();
Imc.Reset();
Atch.Reset();
Shp.Reset();
Atr.Reset();
GlobalEqp.Clear();
}
@ -57,6 +63,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Rsp.Dispose();
Imc.Dispose();
Atch.Dispose();
Shp.Dispose();
Atr.Dispose();
}
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
@ -71,6 +79,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod),
AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod),
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
_ => false,
};
@ -92,6 +102,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
ImcIdentifier i => Imc.RevertMod(i, out mod),
RspIdentifier i => Rsp.RevertMod(i, out mod),
AtchIdentifier i => Atch.RevertMod(i, out mod),
ShpIdentifier i => Shp.RevertMod(i, out mod),
AtrIdentifier i => Atr.RevertMod(i, out mod),
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
_ => (mod = null) != null,
};
@ -108,6 +120,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e),
AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e),
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
_ => false,
};

View file

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

View file

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

View file

@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable
return;
var collection = _resolver.PlayerCollection();
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
_collections.SetCollection(collection, CollectionType.Current);
if (collection.Identity.Id == Guid.Empty)
{
Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
}
else
{
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
_collections.SetCollection(collection, CollectionType.Current);
}
}

View file

@ -1,8 +1,8 @@
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.GameData.Actors;

View file

@ -1,4 +1,4 @@
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Mods;

View file

@ -1,6 +1,6 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;

View file

@ -1,6 +1,6 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods.Manager;

View file

@ -1,4 +1,4 @@
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Communication;

View file

@ -1,4 +1,5 @@
using OtterGui;
using OtterGui.Extensions;
using Penumbra.Collections.Manager;
namespace Penumbra.Collections;

View file

@ -1,7 +1,7 @@
using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api.Api;
@ -75,20 +75,21 @@ public class CommandHandler : IDisposable, IApiService
_ = argumentList[0].ToLowerInvariant() switch
{
"window" => ToggleWindow(arguments),
"enable" => SetPenumbraState(arguments, true),
"disable" => SetPenumbraState(arguments, false),
"toggle" => SetPenumbraState(arguments, null),
"reload" => Reload(arguments),
"redraw" => Redraw(arguments),
"lockui" => SetUiLockState(arguments),
"size" => SetUiMinimumSize(arguments),
"debug" => SetDebug(arguments),
"collection" => SetCollection(arguments),
"mod" => SetMod(arguments),
"bulktag" => SetTag(arguments),
"knowledge" => HandleKnowledge(arguments),
_ => PrintHelp(argumentList[0]),
"window" => ToggleWindow(arguments),
"enable" => SetPenumbraState(arguments, true),
"disable" => SetPenumbraState(arguments, false),
"toggle" => SetPenumbraState(arguments, null),
"reload" => Reload(arguments),
"redraw" => Redraw(arguments),
"lockui" => SetUiLockState(arguments),
"size" => SetUiMinimumSize(arguments),
"debug" => SetDebug(arguments),
"collection" => SetCollection(arguments),
"mod" => SetMod(arguments),
"bulktag" => SetTag(arguments),
"clearsettings" => ClearSettings(arguments),
"knowledge" => HandleKnowledge(arguments),
_ => PrintHelp(argumentList[0]),
};
}
@ -126,6 +127,21 @@ public class CommandHandler : IDisposable, IApiService
_chat.Print(new SeStringBuilder()
.AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.")
.BuiltString);
_chat.Print(new SeStringBuilder()
.AddCommand("clearsettings",
"Clear all temporary settings applied manually through Penumbra in the current or all collections. Use with 'all' parameter for all.")
.BuiltString);
return true;
}
private bool ClearSettings(string arguments)
{
if (arguments.Trim().ToLowerInvariant() is "all")
foreach (var collection in _collectionManager.Storage)
_collectionEditor.ClearTemporarySettings(collection);
else
_collectionEditor.ClearTemporarySettings(_collectionManager.Active.Current);
return true;
}
@ -416,7 +432,8 @@ public class CommandHandler : IDisposable, IApiService
var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (split2.Length < 2)
{
_chat.Print("Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options.");
_chat.Print(
"Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options.");
return false;
}

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
using Dalamud.Configuration;
using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Filesystem;
using OtterGui.Services;
using OtterGui.Widgets;
@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
public record PcpSettings
{
public bool CreateCollection { get; set; } = true;
public bool AssignCollection { get; set; } = true;
public bool AllowIpc { get; set; } = true;
public bool DisableHandling { get; set; } = false;
public string FolderName { get; set; } = "PCP";
}
[Serializable]
public class Configuration : IPluginConfiguration, ISavable, IService
{
@ -44,6 +53,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { get; set; } = string.Empty;
public string WatchDirectory { get; set; } = string.Empty;
public bool? UseCrashHandler { get; set; } = null;
public bool OpenWindowAtStart { get; set; } = false;
@ -67,9 +77,13 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public bool DefaultTemporaryMode { get; set; } = false;
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
public int OptionGroupCollapsibleMin { get; set; } = 5;
public bool EnableDirectoryWatch { get; set; } = false;
public bool EnableAutomaticModImport { get; set; } = false;
public bool EnableCustomShapes { get; set; } = true;
public PcpSettings PcpSettings = new();
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
public int OptionGroupCollapsibleMin { get; set; } = 5;
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);
@ -84,9 +98,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
[JsonProperty(Order = int.MaxValue)]
public ISortMode<Mod> SortMode = ISortMode<Mod>.FoldersFirst;
public bool ScaleModSelector { get; set; } = false;
public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize;
public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize;
public bool OpenFoldersByDefault { get; set; } = false;
public int SingleGroupRadioMax { get; set; } = 2;
public string DefaultImportFolder { get; set; } = string.Empty;
@ -94,6 +105,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public string QuickMoveFolder2 { get; set; } = string.Empty;
public string QuickMoveFolder3 { get; set; } = string.Empty;
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control);
public bool PrintSuccessfulCommandsToChat { get; set; } = true;
public bool AutoDeduplicateOnImport { get; set; } = true;
public bool AutoReduplicateUiOnImport { get; set; } = true;

View file

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

View file

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

View file

@ -2,7 +2,7 @@ using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using Lumina.Extensions;
using OtterGui;
using OtterGui.Extensions;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Geometry;
@ -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.
// TODO: While this assumption is safe, it would be sensible to actually check.
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
return new VertexPositionNormalTangent(
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
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 = (
uvCount,
usages.ContainsKey(MdlFile.VertexUsage.Color)
nColors
);
return materialUsages switch
{
(3, true) => typeof(VertexTexture3ColorFfxiv),
(3, false) => typeof(VertexTexture3),
(2, true) => typeof(VertexTexture2ColorFfxiv),
(2, false) => typeof(VertexTexture2),
(1, true) => typeof(VertexTexture1ColorFfxiv),
(1, false) => typeof(VertexTexture1),
(0, true) => typeof(VertexColorFfxiv),
(0, false) => typeof(VertexEmpty),
(3, 2) => typeof(VertexTexture3Color2Ffxiv),
(3, 1) => typeof(VertexTexture3ColorFfxiv),
(3, 0) => typeof(VertexTexture3),
(2, 2) => typeof(VertexTexture2Color2Ffxiv),
(2, 1) => typeof(VertexTexture2ColorFfxiv),
(2, 0) => typeof(VertexTexture2),
(1, 2) => typeof(VertexTexture1Color2Ffxiv),
(1, 1) => typeof(VertexTexture1ColorFfxiv),
(1, 0) => typeof(VertexTexture1),
(0, 2) => typeof(VertexColor2Ffxiv),
(0, 1) => typeof(VertexColorFfxiv),
(0, 0) => typeof(VertexEmpty),
_ => throw _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))
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
if (_materialType == typeof(VertexColor2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1));
}
if (_materialType == typeof(VertexTexture1))
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
@ -428,6 +441,16 @@ public class MeshExporter
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
);
if (_materialType == typeof(VertexTexture1Color2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture1Color2Ffxiv(
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
ToVector4(color0),
ToVector4(color1)
);
}
// XIV packs two UVs into a single vec4 attribute.
if (_materialType == typeof(VertexTexture2))
@ -448,6 +471,20 @@ public class MeshExporter
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
);
}
if (_materialType == typeof(VertexTexture2Color2Ffxiv))
{
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture2Color2Ffxiv(
new Vector2(uv.X, uv.Y),
new Vector2(uv.Z, uv.W),
ToVector4(color0),
ToVector4(color1)
);
}
if (_materialType == typeof(VertexTexture3))
{
// Not 100% sure about this
@ -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}");
}
@ -537,6 +589,17 @@ public class MeshExporter
return list[0];
}
/// <summary> Check that the list has length 2 for any case where this is expected and return both entries. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (T First, T Second) GetBothSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
{
var list = attributes[usage];
if (list.Count != 2)
throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2.");
return (list[0], list[1]);
}
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private static Vector2 ToVector2(object data)

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
using Dalamud.Plugin.Services;
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Services;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using Penumbra.Services;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip;
@ -96,17 +97,36 @@ public partial class TexToolsImporter
_token.ThrowIfCancellationRequested();
var oldName = _currentModDirectory.FullName;
// Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
if (leadDir)
// Try renaming the folder three times because sometimes we get AccessDenied here for some unknown reason.
const int numTries = 3;
for (var i = 1;; ++i)
{
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false);
Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName);
Directory.Delete(oldName);
}
else
{
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false);
Directory.Move(oldName, _currentModDirectory.FullName);
// Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
try
{
if (leadDir)
{
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false);
Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName);
Directory.Delete(oldName);
}
else
{
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false);
Directory.Move(oldName, _currentModDirectory.FullName);
}
}
catch (IOException io)
{
if (i == numTries)
throw;
Penumbra.Log.Warning($"Error when renaming the extracted mod, try {i}/{numTries}: {io.Message}.");
continue;
}
break;
}
_currentModDirectory.Refresh();
@ -127,6 +147,9 @@ public partial class TexToolsImporter
case ".mtrl":
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
break;
case ".tex":
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
break;
default:
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
break;

View file

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

View file

@ -1,5 +1,5 @@
using Newtonsoft.Json;
using OtterGui;
using OtterGui.Extensions;
using Penumbra.Api.Enums;
using Penumbra.Import.Structs;
using Penumbra.Mods;
@ -259,6 +259,7 @@ public partial class TexToolsImporter
{
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data),
".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data),
_ => data.Data,
};

View file

@ -2,7 +2,6 @@ using Lumina.Extensions;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Import.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Import;
@ -19,9 +18,7 @@ public partial class TexToolsMeta
var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot);
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask;
var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask;
if (_keepDefault || def != value)
MetaManipulations.TryAdd(identifier, value);
MetaManipulations.TryAdd(identifier, value);
}
// Deserialize and check Eqdp Entries and add them to the list if they are non-default.
@ -41,11 +38,9 @@ public partial class TexToolsMeta
continue;
var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr);
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask;
if (_keepDefault || def != value)
MetaManipulations.TryAdd(identifier, value);
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
MetaManipulations.TryAdd(identifier, value);
}
}
@ -55,10 +50,9 @@ public partial class TexToolsMeta
if (data == null)
return;
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
if (_keepDefault || value != def)
MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value);
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
var identifier = new GmpIdentifier(metaFileInfo.PrimaryId);
MetaManipulations.TryAdd(identifier, value);
}
// Deserialize and check Est Entries and add them to the list if they are non-default.
@ -86,9 +80,7 @@ public partial class TexToolsMeta
continue;
var identifier = new EstIdentifier(id, type, gr);
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
if (_keepDefault || def != value)
MetaManipulations.TryAdd(identifier, value);
MetaManipulations.TryAdd(identifier, value);
}
}
@ -108,15 +100,10 @@ public partial class TexToolsMeta
{
var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId,
metaFileInfo.EquipSlot, metaFileInfo.SecondaryType);
var file = new ImcFile(_metaFileManager, identifier);
var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
foreach (var value in values)
{
identifier = identifier with { Variant = (Variant)i };
var def = file.GetEntry(partIdx, (Variant)i);
if (_keepDefault || def != value && identifier.Validate())
MetaManipulations.TryAdd(identifier, value);
MetaManipulations.TryAdd(identifier, value);
++i;
}
}

View file

@ -1,6 +1,5 @@
using Penumbra.GameData.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Import;
@ -8,7 +7,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta
{
// Parse a single rgsp file.
public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data, bool keepDefault)
public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data)
{
if (data.Length != 45 && data.Length != 42)
{
@ -70,9 +69,7 @@ public partial class TexToolsMeta
void Add(RspAttribute attribute, float value)
{
var identifier = new RspIdentifier(subRace, attribute);
var def = CmpFile.GetDefault(manager, subRace, attribute);
if (keepDefault || value != def.Value)
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
}
}
}

View file

@ -23,15 +23,11 @@ public partial class TexToolsMeta
// The info class determines the files or table locations the changes need to apply to from the filename.
public readonly uint Version;
public readonly string FilePath;
public readonly MetaDictionary MetaManipulations = new();
private readonly bool _keepDefault;
public readonly MetaDictionary MetaManipulations = new();
private readonly MetaFileManager _metaFileManager;
public TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte[] data, bool keepDefault)
public TexToolsMeta(GamePathParser parser, byte[] data)
{
_metaFileManager = metaFileManager;
_keepDefault = keepDefault;
try
{
using var reader = new BinaryReader(new MemoryStream(data));
@ -79,7 +75,6 @@ public partial class TexToolsMeta
private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version)
{
_metaFileManager = metaFileManager;
FilePath = filePath;
Version = version;
}

View file

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

View file

@ -6,7 +6,10 @@ public partial class CombinedTexture : IDisposable
{
AsIs,
Bitmap,
BC1,
BC3,
BC4,
BC5,
BC7,
}

View file

@ -61,6 +61,76 @@ public static class TexFileParser
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)
{
fixed (byte* ptr = image.Pixels)

View file

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

View file

@ -1,3 +1,4 @@
using Dalamud.Interface;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services;
@ -6,15 +7,17 @@ using OtterGui.Log;
using OtterGui.Services;
using OtterGui.Tasks;
using OtterTex;
using SharpDX.Direct3D11;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.PixelFormats;
using DxgiDevice = SharpDX.DXGI.Device;
using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures;
public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider)
public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider, IUiBuilder uiBuilder)
: SingleTaskQueue, IDisposable, IService
{
private readonly Logger _logger = logger;
@ -201,8 +204,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
rgba, width, height),
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, width, height),
_ => throw new Exception("Wrong save type."),
};
@ -320,7 +326,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
}
/// <summary> Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. </summary>
public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null,
public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel, byte[]? rgba = null,
int width = 0, int height = 0)
{
switch (input.Type.ReduceToBehaviour())
@ -331,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
cancel.ThrowIfCancellationRequested();
var dds = ConvertToDds(rgba, width, height).AsDds!;
cancel.ThrowIfCancellationRequested();
return CreateCompressed(dds, mipMaps, bc7, cancel);
return CreateCompressed(dds, mipMaps, format, cancel);
}
case TextureType.Dds:
{
var scratch = input.AsDds!;
return CreateCompressed(scratch, mipMaps, bc7, cancel);
return CreateCompressed(scratch, mipMaps, format, cancel);
}
default: return new BaseImage();
}
@ -384,9 +390,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
}
/// <summary> Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. </summary>
public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel)
public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel)
{
var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm;
if (input.Meta.Format == format)
return input;
@ -398,6 +403,16 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
input = AddMipMaps(input, mipMaps);
cancel.ThrowIfCancellationRequested();
// 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)
{
var device = new Device(uiBuilder.DeviceHandle);
var dxgiDevice = device.QueryInterface<DxgiDevice>();
using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel);
return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel);
}
return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel);
}

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);
public ResolveData SoundData
=> _animationLoadData.Value;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public ResolveData SetSoundData(ResolveData data)
{

View file

@ -25,7 +25,7 @@ public unsafe class AtchCallerHook1 : FastHook<AtchCallerHook1.Delegate>, IDispo
private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel)
{
var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true);
var collection = playerModel.Valid ? _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true) : _collectionResolver.DefaultCollection;
_metaState.AtchCollection.Push(collection);
Task.Result.Original(data, slot, unk, playerModel);
_metaState.AtchCollection.Pop();

View file

@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
{
_collectionResolver = collectionResolver;
_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);

View file

@ -2,7 +2,6 @@ using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.Interop.Hooks.Objects;
@ -13,7 +12,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBa
/// <seealso cref="PathResolving.DrawObjectState.OnCharacterBaseDestructor"/>
DrawObjectState = 0,
/// <seealso cref="ModEditWindow.MtrlTab.UnbindFromDrawObjectMaterialInstances"/>
/// <seealso cref="UI.AdvancedWindow.Materials.MtrlTab.UnbindFromDrawObjectMaterialInstances"/>
MtrlTab = -1000,
}
@ -42,7 +41,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBa
private nint Detour(CharacterBase* characterBase)
{
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)characterBase:X}.");
Penumbra.Log.Excessive($"[{Name}] Triggered with 0x{(nint)characterBase:X}.");
Invoke(characterBase);
return _task.Result.Original(characterBase);
}

View file

@ -15,6 +15,9 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, Char
/// <seealso cref="PathResolving.IdentifiedCollectionCache"/>
IdentifiedCollectionCache = 0,
/// <seealso cref="PathResolving.DrawObjectState.OnCharacterDestructor"/>
DrawObjectState = 0,
}
public CharacterDestructor(HookManager hooks)
@ -42,7 +45,7 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, Char
private void Detour(Character* character)
{
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)character:X}.");
Penumbra.Log.Excessive($"[{Name}] Triggered with 0x{(nint)character:X}.");
Invoke(character);
_task.Result.Original(character);
}

View file

@ -35,14 +35,14 @@ public sealed unsafe class WeaponReload : EventWrapperPtr<DrawDataContainer, Cha
public bool Finished
=> _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;
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));
_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);
}

View file

@ -0,0 +1,85 @@
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Interop;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta;
namespace Penumbra.Interop.Hooks.PostProcessing;
/// <summary>
/// Triggered whenever a model recomputes its attribute masks.
/// <list type="number">
/// <item>Parameter is the game object that recomputed its attributes. </item>
/// <item>Parameter is the draw object on which the recomputation was called. </item>
/// <item>Parameter is the collection associated with the game object. </item>
/// <item>Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. </item>
/// </list> </summary>
public sealed unsafe class AttributeHook : EventWrapper<Actor, Model, ModCollection, AttributeHook.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="ShapeAttributeManager.OnAttributeComputed"/>
ShapeAttributeManager = 0,
}
private readonly CollectionResolver _resolver;
private readonly Configuration _config;
public AttributeHook(HookManager hooks, Configuration config, CollectionResolver resolver)
: base("Update Model Attributes")
{
_config = config;
_resolver = resolver;
_task = hooks.CreateHook<Delegate>(Name, Sigs.UpdateAttributes, Detour, config.EnableCustomShapes);
}
private readonly Task<Hook<Delegate>> _task;
public nint Address
=> _task.Result.Address;
public void Enable()
=> SetState(true);
public void Disable()
=> SetState(false);
public void SetState(bool enabled)
{
if (_config.EnableCustomShapes == enabled)
return;
_config.EnableCustomShapes = enabled;
_config.Save();
if (enabled)
_task.Result.Enable();
else
_task.Result.Disable();
}
public Task Awaiter
=> _task;
public bool Finished
=> _task.IsCompletedSuccessfully;
private delegate void Delegate(Human* human);
private void Detour(Human* human)
{
_task.Result.Original(human);
var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true);
var identifiedActor = resolveData.AssociatedGameObject;
var identifiedCollection = resolveData.ModCollection;
Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{identifiedActor:X}).");
Invoke(identifiedActor, human, identifiedCollection);
}
protected override void Dispose(bool disposing)
=> _task.Result.Dispose();
}

View file

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

View file

@ -193,7 +193,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
if (shpk == null)
return;
var shpkName = mtrl->ShpkNameSpan;
var shpkName = mtrl->ShpkName.AsSpan();
var shpkState = GetStateForHumanSetup(shpkName)
?? GetStateForHumanRender(shpkName)
?? GetStateForModelRendererRender(shpkName)
@ -217,7 +217,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
}
private ModdedShaderPackageState? GetStateForHumanSetup(MaterialResourceHandle* mtrlResource)
=> mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan);
=> mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkName.AsSpan());
private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan<byte> shpkName)
=> CharacterStockingsShpkName.SequenceEqual(shpkName) ? _characterStockingsState : null;
@ -227,7 +227,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
=> _characterStockingsState.MaterialCount;
private ModdedShaderPackageState? GetStateForHumanRender(MaterialResourceHandle* mtrlResource)
=> mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan);
=> mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkName.AsSpan());
private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan<byte> shpkName)
=> SkinShpkName.SequenceEqual(shpkName) ? _skinState : null;
@ -237,7 +237,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
=> _skinState.MaterialCount;
private ModdedShaderPackageState? GetStateForModelRendererRender(MaterialResourceHandle* mtrlResource)
=> mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkNameSpan);
=> mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkName.AsSpan());
private ModdedShaderPackageState? GetStateForModelRendererRender(ReadOnlySpan<byte> shpkName)
{
@ -264,7 +264,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
+ _hairMaskState.MaterialCount;
private ModdedShaderPackageState? GetStateForModelRendererUnk(MaterialResourceHandle* mtrlResource)
=> mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkNameSpan);
=> mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkName.AsSpan());
private ModdedShaderPackageState? GetStateForModelRendererUnk(ReadOnlySpan<byte> shpkName)
{
@ -480,7 +480,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
if (material == null)
return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id);
var shpkState = GetStateForColorTable(thisPtr->ShpkNameSpan);
var shpkState = GetStateForColorTable(thisPtr->ShpkName.AsSpan());
if (shpkState == null || shpkState.MaterialCount == 0)
return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id);

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