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_xaml_x_key_attribute_disallowed_highlighting=error
resharper_xml_doc_comment_syntax_problem_highlighting=warning resharper_xml_doc_comment_syntax_problem_highlighting=warning
resharper_xunit_xunit_test_with_console_output_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}] [*.{cshtml,htm,html,proto,razor}]
indent_style=tab indent_style=tab

View file

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

View file

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

View file

@ -15,7 +15,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: '8.x.x' dotnet-version: '9.x.x'
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Download Dalamud - 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; 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; namespace Penumbra.CrashHandler.Buffers;

View file

@ -1,5 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.MemoryMappedFiles; using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text; 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; namespace Penumbra.CrashHandler.Buffers;

View file

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using Penumbra.CrashHandler.Buffers; using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler; 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; using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler; namespace Penumbra.CrashHandler;

View file

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

View file

@ -1,20 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <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>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -25,4 +11,8 @@
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<Use_DalamudPackager>false</Use_DalamudPackager>
</PropertyGroup>
</Project> </Project>

View file

@ -1,4 +1,6 @@
using System.Diagnostics; using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json; using System.Text.Json;
namespace Penumbra.CrashHandler; 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 ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
.github\workflows\build.yml = .github\workflows\build.yml .github\workflows\build.yml = .github\workflows\build.yml
Penumbra\Penumbra.json = Penumbra\Penumbra.json
.github\workflows\release.yml = .github\workflows\release.yml .github\workflows\release.yml = .github\workflows\release.yml
repo.json = repo.json repo.json = repo.json
.github\workflows\test_release.yml = .github\workflows\test_release.yml .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\group_single.json = schemas\structs\group_single.json
schemas\structs\manipulation.json = schemas\structs\manipulation.json schemas\structs\manipulation.json = schemas\structs\manipulation.json
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
@ -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_gmp.json = schemas\structs\meta_gmp.json
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.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 schemas\structs\option.json = schemas\structs\option.json
EndProjectSection EndProjectSection
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU Release|x64 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly CutsceneService _cutsceneService; private readonly CutsceneService _cutsceneService;
private readonly ResourceLoader _resourceLoader; private readonly ResourceLoader _resourceLoader;
public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService, public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
ResourceLoader resourceLoader) ResourceLoader resourceLoader, DrawObjectState drawObjectState)
{ {
_communicator = communicator; _communicator = communicator;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_cutsceneService = cutsceneService; _cutsceneService = cutsceneService;
_resourceLoader = resourceLoader; _resourceLoader = resourceLoader;
_drawObjectState = drawObjectState;
_resourceLoader.ResourceLoaded += OnResourceLoaded; _resourceLoader.ResourceLoaded += OnResourceLoaded;
_resourceLoader.PapRequested += OnPapRequested; _resourceLoader.PapRequested += OnPapRequested;
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
@ -67,6 +69,30 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
public int GetCutsceneParentIndex(int actorIdx) public int GetCutsceneParentIndex(int actorIdx)
=> _cutsceneService.GetParentIndex(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) public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx) => _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
? PenumbraApiEc.Success ? 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.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair<ShpIdentifier, ShpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair<AtrIdentifier, AtrEntry>(kvp.Key, kvp.Value.Entry)));
} }
return Functions.ToCompressedBase64(array, 0); return Functions.ToCompressedBase64(array, 0);
@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
} }
WriteCache(zipStream, cache.Atch); WriteCache(zipStream, cache.Atch);
WriteCache(zipStream, cache.Shp);
WriteCache(zipStream, cache.Atr);
} }
} }
@ -140,6 +144,86 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
} }
} }
public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8);
public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8);
public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P';
public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8);
public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8);
public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8);
public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P';
public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H';
public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8);
public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8);
private static unsafe string CompressMetaManipulationsV2(ModCollection? collection)
{
using var ms = new MemoryStream();
ms.Capacity = 1024;
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
{
zipStream.Write((byte)2);
zipStream.Write("META0002"u8);
if (collection?.MetaCache is { } cache)
{
WriteCache(zipStream, cache.Imc, ImcKey);
WriteCache(zipStream, cache.Eqp, EqpKey);
WriteCache(zipStream, cache.Eqdp, EqdpKey);
WriteCache(zipStream, cache.Est, EstKey);
WriteCache(zipStream, cache.Rsp, RspKey);
WriteCache(zipStream, cache.Gmp, GmpKey);
cache.GlobalEqp.EnterReadLock();
try
{
if (cache.GlobalEqp.Count > 0)
{
zipStream.Write(GeqpKey);
zipStream.Write(cache.GlobalEqp.Count);
foreach (var (globalEqp, _) in cache.GlobalEqp)
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
}
}
finally
{
cache.GlobalEqp.ExitReadLock();
}
WriteCache(zipStream, cache.Atch, AtchKey);
WriteCache(zipStream, cache.Shp, ShpKey);
WriteCache(zipStream, cache.Atr, AtrKey);
}
}
ms.Flush();
ms.Position = 0;
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
return Convert.ToBase64String(data);
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache, uint label)
where TKey : unmanaged, IMetaIdentifier
where TValue : unmanaged
{
metaCache.EnterReadLock();
try
{
if (metaCache.Count <= 0)
return;
stream.Write(label);
stream.Write(metaCache.Count);
foreach (var (identifier, (_, value)) in metaCache)
{
stream.Write(identifier);
stream.Write(value);
}
}
finally
{
metaCache.ExitReadLock();
}
}
}
/// <summary> /// <summary>
/// Convert manipulations from a transmitted base64 string to actual manipulations. /// Convert manipulations from a transmitted base64 string to actual manipulations.
/// The empty string is treated as an empty set. /// The empty string is treated as an empty set.
@ -170,6 +254,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
{ {
case 0: return ConvertManipsV0(data, out manips); case 0: return ConvertManipsV0(data, out manips);
case 1: return ConvertManipsV1(data, out manips); case 1: return ConvertManipsV1(data, out manips);
case 2: return ConvertManipsV2(data, out manips);
default: default:
Penumbra.Log.Debug($"Invalid version for manipulations: {version}."); Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
manips = null; manips = null;
@ -185,6 +270,131 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
} }
} }
private static bool ConvertManipsV2(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (!data.StartsWith("META0002"u8))
{
Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix.");
manips = null;
return false;
}
manips = new MetaDictionary();
var r = new SpanBinaryReader(data[8..]);
while (r.Remaining > 4)
{
var prefix = r.ReadUInt32();
var count = r.Remaining > 4 ? r.ReadInt32() : 0;
if (count is 0)
continue;
switch (prefix)
{
case ImcKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<ImcIdentifier>();
var value = r.Read<ImcEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EqpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EqpIdentifier>();
var value = r.Read<EqpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EqdpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EqdpIdentifier>();
var value = r.Read<EqdpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EstKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EstIdentifier>();
var value = r.Read<EstEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case RspKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<RspIdentifier>();
var value = r.Read<RspEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case GmpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<GmpIdentifier>();
var value = r.Read<GmpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case GeqpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<GlobalEqpManipulation>();
if (!identifier.Validate() || !manips.TryAdd(identifier))
return false;
}
break;
case AtchKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<AtchIdentifier>();
var value = r.Read<AtchEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case ShpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<ShpIdentifier>();
var value = r.Read<ShpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case AtrKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<AtrIdentifier>();
var value = r.Read<AtrEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
}
}
return true;
}
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips) private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{ {
if (!data.StartsWith("META0001"u8)) if (!data.StartsWith("META0001"u8))
@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
if (!identifier.Validate() || !manips.TryAdd(identifier, value)) if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false; return false;
} }
// Shp and Atr was added later
if (r.Position < r.Count)
{
var shpCount = r.ReadInt32();
for (var i = 0; i < shpCount; ++i)
{
var identifier = r.Read<ShpIdentifier>();
var value = r.Read<ShpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var atrCount = r.ReadInt32();
for (var i = 0; i < atrCount; ++i)
{
var identifier = r.Read<AtrIdentifier>();
var value = r.Read<AtrEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
}
} }
return true; return true;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,23 +1,53 @@
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Interop;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
namespace Penumbra.Api.Api; namespace Penumbra.Api.Api;
public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
{ {
public void RedrawObject(int gameObjectIndex, RedrawType setting) public void RedrawObject(int gameObjectIndex, RedrawType setting)
=> redrawService.RedrawObject(gameObjectIndex, setting); {
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting));
}
public void RedrawObject(string name, RedrawType 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) public void RedrawObject(IGameObject? gameObject, RedrawType setting)
=> redrawService.RedrawObject(gameObject, setting); {
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting));
}
public void RedrawAll(RedrawType 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 public event GameObjectRedrawnDelegate? GameObjectRedrawn
{ {

View file

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

View file

@ -1,9 +1,11 @@
using Dalamud.Plugin.Services;
using EmbedIO; using EmbedIO;
using EmbedIO.Routing; using EmbedIO.Routing;
using EmbedIO.WebApi; using EmbedIO.WebApi;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Api; using Penumbra.Api.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Mods.Settings;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -12,23 +14,28 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller : WebApiController private partial class Controller : WebApiController
{ {
// @formatter:off // @formatter:off
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); [Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
[Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll(); [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); [Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod(); [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow(); [Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
// @formatter:on // @formatter:on
} }
public const string Prefix = "http://localhost:42069/"; public const string Prefix = "http://localhost:42069/";
private readonly IPenumbraApi _api; private readonly IPenumbraApi _api;
private readonly IFramework _framework;
private WebServer? _server; 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) if (config.EnableHttpApi)
CreateWebServer(); CreateWebServer();
} }
@ -44,7 +51,7 @@ public class HttpApi : IDisposable, IApiService
.WithUrlPrefix(Prefix) .WithUrlPrefix(Prefix)
.WithMode(HttpListenerMode.EmbedIO)) .WithMode(HttpListenerMode.EmbedIO))
.WithCors(Prefix) .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.StateChanged += (_, e) => Penumbra.Log.Information($"WebServer New State - {e.NewState}");
_server.RunAsync(); _server.RunAsync();
@ -59,60 +66,96 @@ public class HttpApi : IDisposable, IApiService
public void Dispose() public void Dispose()
=> ShutdownWebServer(); => ShutdownWebServer();
private partial class Controller private partial class Controller(IPenumbraApi api, IFramework framework)
{ {
private readonly IPenumbraApi _api; public partial string GetModDirectory()
{
public Controller(IPenumbraApi api) Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
=> _api = api; return api.PluginState.GetModDirectory();
}
public partial object? GetMods() public partial object? GetMods()
{ {
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
return _api.Mods.GetModList(); return api.Mods.GetModList();
} }
public async partial Task Redraw() public async partial Task Redraw()
{ {
var data = await HttpContext.GetRequestDataAsync<RedrawData>(); var data = await HttpContext.GetRequestDataAsync<RedrawData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}."); Penumbra.Log.Debug($"[HTTP] [{Environment.CurrentManagedThreadId}] {nameof(Redraw)} triggered with {data}.");
if (data.ObjectTableIndex >= 0) await framework.RunOnFrameworkThread(() =>
_api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type); {
else if (data.ObjectTableIndex >= 0)
_api.Redraw.RedrawAll(data.Type); 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."); 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() 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}."); 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. // 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. // AddMod returns Success if the mod is already loaded.
if (data.Path.Length != 0) 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. // 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() 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}."); Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}.");
if (data.Path.Length != 0) if (data.Path.Length != 0)
_api.Mods.InstallMod(data.Path); api.Mods.InstallMod(data.Path);
} }
public partial void OpenWindow() public partial void OpenWindow()
{ {
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); 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) 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) private record ModInstallData(string Path)
{ {
public ModInstallData() public ModInstallData()
@ -135,5 +185,19 @@ public class HttpApi : IDisposable, IApiService
: this(string.Empty, RedrawType.Redraw, -1) : this(string.Empty, RedrawType.Redraw, -1)
{ } { }
} }
private record SetModSettingsData(
Guid? CollectionId,
string ModPath,
string ModName,
bool? Inherit,
bool? State,
int? Priority,
Dictionary<string, List<string>>? Settings)
{
public SetModSettingsData()
: this(null, string.Empty, string.Empty, null, null, null, null)
{}
}
} }
} }

View file

@ -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.CreatingCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState), IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
IpcSubscribers.GameObjectResourcePathResolved.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.GetPlayerMetaManipulations.Provider(pi, api.Meta),
IpcSubscribers.GetMetaManipulations.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.ModDeleted.Provider(pi, api.Mods),
IpcSubscribers.ModAdded.Provider(pi, api.Mods), IpcSubscribers.ModAdded.Provider(pi, api.Mods),
IpcSubscribers.ModMoved.Provider(pi, api.Mods), IpcSubscribers.ModMoved.Provider(pi, api.Mods),
IpcSubscribers.CreatingPcp.Provider(pi, api.Mods),
IpcSubscribers.ParsingPcp.Provider(pi, api.Mods),
IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.GetModPath.Provider(pi, api.Mods),
IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
@ -78,10 +82,13 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState), IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState), IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw), IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw), IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve), IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve), IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
@ -8,6 +7,7 @@ using Penumbra.Mods.Editor;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Util; using Penumbra.Util;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using OtterGui.Extensions;
namespace Penumbra.Collections.Cache; namespace Penumbra.Collections.Cache;
@ -245,6 +245,10 @@ public sealed class CollectionCache : IDisposable
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Atch) foreach (var (identifier, entry) in files.Manipulations.Atch)
AddManipulation(mod, identifier, entry); 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) foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!); 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.BodyHideGlovesL)
: entry.HasFlag(EqpEntry.BodyHideGlovesM); : entry.HasFlag(EqpEntry.BodyHideGlovesM);
return testFlag return testFlag
? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS ? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS
: entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS); : entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]

View file

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

View file

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

View file

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

View file

@ -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; return;
var collection = _resolver.PlayerCollection(); var collection = _resolver.PlayerCollection();
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); if (collection.Identity.Id == Guid.Empty)
_collections.SetCollection(collection, CollectionType.Current); {
Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
}
else
{
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
_collections.SetCollection(collection, CollectionType.Current);
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImGuiNET; using Dalamud.Bindings.ImGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Api; using Penumbra.Api.Api;
@ -75,20 +75,21 @@ public class CommandHandler : IDisposable, IApiService
_ = argumentList[0].ToLowerInvariant() switch _ = argumentList[0].ToLowerInvariant() switch
{ {
"window" => ToggleWindow(arguments), "window" => ToggleWindow(arguments),
"enable" => SetPenumbraState(arguments, true), "enable" => SetPenumbraState(arguments, true),
"disable" => SetPenumbraState(arguments, false), "disable" => SetPenumbraState(arguments, false),
"toggle" => SetPenumbraState(arguments, null), "toggle" => SetPenumbraState(arguments, null),
"reload" => Reload(arguments), "reload" => Reload(arguments),
"redraw" => Redraw(arguments), "redraw" => Redraw(arguments),
"lockui" => SetUiLockState(arguments), "lockui" => SetUiLockState(arguments),
"size" => SetUiMinimumSize(arguments), "size" => SetUiMinimumSize(arguments),
"debug" => SetDebug(arguments), "debug" => SetDebug(arguments),
"collection" => SetCollection(arguments), "collection" => SetCollection(arguments),
"mod" => SetMod(arguments), "mod" => SetMod(arguments),
"bulktag" => SetTag(arguments), "bulktag" => SetTag(arguments),
"knowledge" => HandleKnowledge(arguments), "clearsettings" => ClearSettings(arguments),
_ => PrintHelp(argumentList[0]), "knowledge" => HandleKnowledge(arguments),
_ => PrintHelp(argumentList[0]),
}; };
} }
@ -126,6 +127,21 @@ public class CommandHandler : IDisposable, IApiService
_chat.Print(new SeStringBuilder() _chat.Print(new SeStringBuilder()
.AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.") .AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.")
.BuiltString); .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; return true;
} }
@ -416,7 +432,8 @@ public class CommandHandler : IDisposable, IApiService
var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (split2.Length < 2) 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; return false;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ using System.Collections.Immutable;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Lumina.Extensions; using Lumina.Extensions;
using OtterGui; using OtterGui.Extensions;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ModelStructs; using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Geometry; using SharpGLTF.Geometry;
@ -364,7 +364,7 @@ public class MeshExporter
return new VertexPositionNormalTangent( return new VertexPositionNormalTangent(
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)),
bitangent bitangent.SanitizeTangent()
); );
} }
@ -390,23 +390,30 @@ public class MeshExporter
} }
} }
usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours);
var nColors = colours?.Count ?? 0;
var materialUsages = ( var materialUsages = (
uvCount, uvCount,
usages.ContainsKey(MdlFile.VertexUsage.Color) nColors
); );
return materialUsages switch return materialUsages switch
{ {
(3, true) => typeof(VertexTexture3ColorFfxiv), (3, 2) => typeof(VertexTexture3Color2Ffxiv),
(3, false) => typeof(VertexTexture3), (3, 1) => typeof(VertexTexture3ColorFfxiv),
(2, true) => typeof(VertexTexture2ColorFfxiv), (3, 0) => typeof(VertexTexture3),
(2, false) => typeof(VertexTexture2), (2, 2) => typeof(VertexTexture2Color2Ffxiv),
(1, true) => typeof(VertexTexture1ColorFfxiv), (2, 1) => typeof(VertexTexture2ColorFfxiv),
(1, false) => typeof(VertexTexture1), (2, 0) => typeof(VertexTexture2),
(0, true) => typeof(VertexColorFfxiv), (1, 2) => typeof(VertexTexture1Color2Ffxiv),
(0, false) => typeof(VertexEmpty), (1, 1) => typeof(VertexTexture1ColorFfxiv),
(1, 0) => typeof(VertexTexture1),
(0, 2) => typeof(VertexColor2Ffxiv),
(0, 1) => typeof(VertexColorFfxiv),
(0, 0) => typeof(VertexEmpty),
_ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), _ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."),
}; };
} }
@ -419,6 +426,12 @@ public class MeshExporter
if (_materialType == typeof(VertexColorFfxiv)) if (_materialType == typeof(VertexColorFfxiv))
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
if (_materialType == typeof(VertexColor2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1));
}
if (_materialType == typeof(VertexTexture1)) if (_materialType == typeof(VertexTexture1))
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
@ -428,6 +441,16 @@ public class MeshExporter
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
); );
if (_materialType == typeof(VertexTexture1Color2Ffxiv))
{
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture1Color2Ffxiv(
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
ToVector4(color0),
ToVector4(color1)
);
}
// XIV packs two UVs into a single vec4 attribute. // XIV packs two UVs into a single vec4 attribute.
if (_materialType == typeof(VertexTexture2)) if (_materialType == typeof(VertexTexture2))
@ -448,6 +471,20 @@ public class MeshExporter
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
); );
} }
if (_materialType == typeof(VertexTexture2Color2Ffxiv))
{
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture2Color2Ffxiv(
new Vector2(uv.X, uv.Y),
new Vector2(uv.Z, uv.W),
ToVector4(color0),
ToVector4(color1)
);
}
if (_materialType == typeof(VertexTexture3)) if (_materialType == typeof(VertexTexture3))
{ {
// Not 100% sure about this // Not 100% sure about this
@ -472,6 +509,21 @@ public class MeshExporter
); );
} }
if (_materialType == typeof(VertexTexture3Color2Ffxiv))
{
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
return new VertexTexture3Color2Ffxiv(
new Vector2(uv0.X, uv0.Y),
new Vector2(uv0.Z, uv0.W),
new Vector2(uv1.X, uv1.Y),
ToVector4(color0),
ToVector4(color1)
);
}
throw _notifier.Exception($"Unknown material type {_skinningType}"); throw _notifier.Exception($"Unknown material type {_skinningType}");
} }
@ -538,6 +590,17 @@ public class MeshExporter
return list[0]; return list[0];
} }
/// <summary> Check that the list has length 2 for any case where this is expected and return both entries. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (T First, T Second) GetBothSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
{
var list = attributes[usage];
if (list.Count != 2)
throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2.");
return (list[0], list[1]);
}
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary> /// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private static Vector2 ToVector2(object data) private static Vector2 ToVector2(object data)
=> data switch => data switch

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ public class SimpleMod
public class ModPackPage public class ModPackPage
{ {
public int PageIndex = 0; public int PageIndex = 0;
public ModGroup[] ModGroups = Array.Empty<ModGroup>(); public ModGroup[] ModGroups = [];
} }
[Serializable] [Serializable]
@ -34,7 +34,7 @@ public class ModGroup
{ {
public string GroupName = string.Empty; public string GroupName = string.Empty;
public GroupType SelectionType = GroupType.Single; public GroupType SelectionType = GroupType.Single;
public OptionList[] OptionList = Array.Empty<OptionList>(); public OptionList[] OptionList = [];
public string Description = string.Empty; public string Description = string.Empty;
} }
@ -44,7 +44,7 @@ public class OptionList
public string Name = string.Empty; public string Name = string.Empty;
public string Description = string.Empty; public string Description = string.Empty;
public string ImagePath = string.Empty; public string ImagePath = string.Empty;
public SimpleMod[] ModsJsons = Array.Empty<SimpleMod>(); public SimpleMod[] ModsJsons = [];
public string GroupName = string.Empty; public string GroupName = string.Empty;
public GroupType SelectionType = GroupType.Single; public GroupType SelectionType = GroupType.Single;
public bool IsChecked = false; public bool IsChecked = false;
@ -59,8 +59,8 @@ public class ExtendedModPack
public string Version = string.Empty; public string Version = string.Empty;
public string Description = DefaultTexToolsData.Description; public string Description = DefaultTexToolsData.Description;
public string Url = string.Empty; public string Url = string.Empty;
public ModPackPage[] ModPackPages = Array.Empty<ModPackPage>(); public ModPackPage[] ModPackPages = [];
public SimpleMod[] SimpleModsList = Array.Empty<SimpleMod>(); public SimpleMod[] SimpleModsList = [];
} }
[Serializable] [Serializable]
@ -72,5 +72,5 @@ public class SimpleModPack
public string Version = string.Empty; public string Version = string.Empty;
public string Description = DefaultTexToolsData.Description; public string Description = DefaultTexToolsData.Description;
public string Url = string.Empty; 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. // Puts out warnings if extension does not correspond to data.
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile) private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
{ {
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar") if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".pcp" or ".zip" or ".7z" or ".rar")
return HandleRegularArchive(modPackFile); return HandleRegularArchive(modPackFile);
using var zfs = modPackFile.OpenRead(); using var zfs = modPackFile.OpenRead();

View file

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Rar; using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.SevenZip;
@ -96,17 +97,36 @@ public partial class TexToolsImporter
_token.ThrowIfCancellationRequested(); _token.ThrowIfCancellationRequested();
var oldName = _currentModDirectory.FullName; 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); // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); try
Directory.Delete(oldName); {
} if (leadDir)
else {
{ _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false);
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false); Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName);
Directory.Move(oldName, _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(); _currentModDirectory.Refresh();
@ -127,6 +147,9 @@ public partial class TexToolsImporter
case ".mtrl": case ".mtrl":
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
break; break;
case ".tex":
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
break;
default: default:
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions); reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
break; break;

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
namespace Penumbra.Import; namespace Penumbra.Import;
@ -8,7 +7,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta public partial class TexToolsMeta
{ {
// Parse a single rgsp file. // 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) if (data.Length != 45 && data.Length != 42)
{ {
@ -70,9 +69,7 @@ public partial class TexToolsMeta
void Add(RspAttribute attribute, float value) void Add(RspAttribute attribute, float value)
{ {
var identifier = new RspIdentifier(subRace, attribute); var identifier = new RspIdentifier(subRace, attribute);
var def = CmpFile.GetDefault(manager, subRace, attribute); ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
if (keepDefault || value != def.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. // The info class determines the files or table locations the changes need to apply to from the filename.
public readonly uint Version; public readonly uint Version;
public readonly string FilePath; public readonly string FilePath;
public readonly MetaDictionary MetaManipulations = new(); public readonly MetaDictionary MetaManipulations = new();
private readonly bool _keepDefault;
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 try
{ {
using var reader = new BinaryReader(new MemoryStream(data)); using var reader = new BinaryReader(new MemoryStream(data));
@ -79,7 +75,6 @@ public partial class TexToolsMeta
private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version) private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version)
{ {
_metaFileManager = metaFileManager;
FilePath = filePath; FilePath = filePath;
Version = version; Version = version;
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
using Dalamud.Interface;
using Dalamud.Interface.Textures; using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@ -6,15 +7,17 @@ using OtterGui.Log;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Tasks; using OtterGui.Tasks;
using OtterTex; using OtterTex;
using SharpDX.Direct3D11;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using DxgiDevice = SharpDX.DXGI.Device;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures; 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 : SingleTaskQueue, IDisposable, IService
{ {
private readonly Logger _logger = logger; private readonly Logger _logger = logger;
@ -201,8 +204,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
rgba, width, height), rgba, width, height),
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, 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."), _ => 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> /// <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) int width = 0, int height = 0)
{ {
switch (input.Type.ReduceToBehaviour()) switch (input.Type.ReduceToBehaviour())
@ -331,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
cancel.ThrowIfCancellationRequested(); cancel.ThrowIfCancellationRequested();
var dds = ConvertToDds(rgba, width, height).AsDds!; var dds = ConvertToDds(rgba, width, height).AsDds!;
cancel.ThrowIfCancellationRequested(); cancel.ThrowIfCancellationRequested();
return CreateCompressed(dds, mipMaps, bc7, cancel); return CreateCompressed(dds, mipMaps, format, cancel);
} }
case TextureType.Dds: case TextureType.Dds:
{ {
var scratch = input.AsDds!; var scratch = input.AsDds!;
return CreateCompressed(scratch, mipMaps, bc7, cancel); return CreateCompressed(scratch, mipMaps, format, cancel);
} }
default: return new BaseImage(); 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> /// <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) if (input.Meta.Format == format)
return input; return input;
@ -398,6 +403,16 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
input = AddMipMaps(input, mipMaps); input = AddMipMaps(input, mipMaps);
cancel.ThrowIfCancellationRequested(); 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); 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); private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
public ResolveData SoundData
=> _animationLoadData.Value;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public ResolveData SetSoundData(ResolveData data) public ResolveData SetSoundData(ResolveData data)
{ {

View file

@ -25,7 +25,7 @@ public unsafe class AtchCallerHook1 : FastHook<AtchCallerHook1.Delegate>, IDispo
private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel) 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); _metaState.AtchCollection.Push(collection);
Task.Result.Original(data, slot, unk, playerModel); Task.Result.Original(data, slot, unk, playerModel);
_metaState.AtchCollection.Pop(); _metaState.AtchCollection.Pop();

View file

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

View file

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

View file

@ -15,6 +15,9 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, Char
/// <seealso cref="PathResolving.IdentifiedCollectionCache"/> /// <seealso cref="PathResolving.IdentifiedCollectionCache"/>
IdentifiedCollectionCache = 0, IdentifiedCollectionCache = 0,
/// <seealso cref="PathResolving.DrawObjectState.OnCharacterDestructor"/>
DrawObjectState = 0,
} }
public CharacterDestructor(HookManager hooks) public CharacterDestructor(HookManager hooks)
@ -42,7 +45,7 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, Char
private void Detour(Character* character) 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); Invoke(character);
_task.Result.Original(character); _task.Result.Original(character);
} }

View file

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

View file

@ -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

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

View file

@ -1,7 +1,7 @@
using System.Text.Unicode; using System.Text.Unicode;
using Dalamud.Hooking; using Dalamud.Hooking;
using Iced.Intel; using Iced.Intel;
using OtterGui; using OtterGui.Extensions;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Swan; using Swan;

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