mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Compare commits
275 commits
testing_1.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb5b01290 | ||
|
|
5dd74297c6 | ||
|
|
ce54aa5d25 | ||
|
|
c4b6e4e00b | ||
|
|
912c183fc6 | ||
|
|
5bf901d0c4 | ||
|
|
cbedc878b9 | ||
|
|
c8cf560fc1 | ||
|
|
f05cb52da2 | ||
|
|
7ed81a9823 | ||
|
|
60aa23efcd | ||
|
|
ebbe957c95 | ||
|
|
300e0e6d84 | ||
|
|
049baa4fe4 | ||
|
|
0881dfde8a | ||
|
|
23c0506cb8 | ||
|
|
699745413e | ||
|
|
eb53f04c6b | ||
|
|
c6b596169c | ||
|
|
a0c3e820b0 | ||
|
|
a59689ebfe | ||
|
|
e9f67a009b | ||
|
|
97c8d82b33 | ||
|
|
c3b00ff426 | ||
|
|
6348c4a639 | ||
|
|
5a6e06df3b | ||
|
|
f5f6dd3246 | ||
|
|
4e788f7c2b | ||
|
|
ad1659caf6 | ||
|
|
18a6ce2a5f | ||
|
|
e68e821b2a | ||
|
|
96764b34ca | ||
|
|
2cf60b78cd | ||
|
|
d59be1e660 | ||
|
|
5503bb32e0 | ||
|
|
f3ec4b2e08 | ||
|
|
b3379a9710 | ||
|
|
8c25ef4b47 | ||
|
|
912020cc3f | ||
|
|
be8987a451 | ||
|
|
f7cf5503bb | ||
|
|
a04a5a071c | ||
|
|
71e24c13c7 | ||
|
|
c0120f81af | ||
|
|
da47c19aeb | ||
|
|
e16800f216 | ||
|
|
79a4fc5904 | ||
|
|
bf90725dd2 | ||
|
|
a14347f73a | ||
|
|
1e07e43498 | ||
|
|
f51f8a7bf8 | ||
|
|
1fca78fa71 | ||
|
|
c8b6325a87 | ||
|
|
6079103505 | ||
|
|
d302a17f1f | ||
|
|
0d64384059 | ||
|
|
10894d451a | ||
|
|
fb34238530 | ||
|
|
8043e6fb6b | ||
|
|
e3b7f72893 | ||
|
|
b7f326e29c | ||
|
|
dad01e1af8 | ||
|
|
10b71930a1 | ||
|
|
23257f94a4 | ||
|
|
83a36ed4cb | ||
|
|
8304579d29 | ||
|
|
24cbc6c5e1 | ||
|
|
41edc23820 | ||
|
|
aa920b5e9b | ||
|
|
87ace28bcf | ||
|
|
5917f5fad1 | ||
|
|
f69c264317 | ||
|
|
a7246b9d98 | ||
|
|
9aff388e21 | ||
|
|
091aff1b8a | ||
|
|
9f8185f67b | ||
|
|
b112d75a27 | ||
|
|
7af81a6c18 | ||
|
|
12a218bb2b | ||
|
|
f6bac93db7 | ||
|
|
155d3d49aa | ||
|
|
9aae2210a2 | ||
|
|
3785a629ce | ||
|
|
02af52671f | ||
|
|
391c9d727e | ||
|
|
ff2b2be953 | ||
|
|
6242b30f93 | ||
|
|
11cd08a9de | ||
|
|
46cfbcb115 | ||
|
|
66543cc671 | ||
|
|
13283c9690 | ||
|
|
bedfb22466 | ||
|
|
13df8b2248 | ||
|
|
93406e4d4e | ||
|
|
8140d08557 | ||
|
|
2b36f39848 | ||
|
|
a69811800d | ||
|
|
3f18ad50de | ||
|
|
6689e326ee | ||
|
|
bdcab22a55 | ||
|
|
f5f4fe7259 | ||
|
|
898963fea5 | ||
|
|
8527bfa29c | ||
|
|
baca3cdec2 | ||
|
|
dc93eba34c | ||
|
|
012052daa0 | ||
|
|
a9546e31ee | ||
|
|
a4a6283e7b | ||
|
|
00c02fd16e | ||
|
|
140d150bb4 | ||
|
|
49a6d935f3 | ||
|
|
692beacc2e | ||
|
|
a953febfba | ||
|
|
c0aa2e36ea | ||
|
|
278bf43b29 | ||
|
|
a97d9e4953 | ||
|
|
30e3cd1f38 | ||
|
|
62e9dc164d | ||
|
|
9fc572ba0c | ||
|
|
3c20b541ce | ||
|
|
1961b03d37 | ||
|
|
1f4ec984b3 | ||
|
|
4981b0348f | ||
|
|
a8c05fc6ee | ||
|
|
3d05662384 | ||
|
|
973814b31b | ||
|
|
a16fd85a7e | ||
|
|
4c0e6d2a67 | ||
|
|
535694e9c8 | ||
|
|
318a41fe52 | ||
|
|
98203e4e8a | ||
|
|
6cba63ac98 | ||
|
|
b48c4f440a | ||
|
|
75f4e66dbf | ||
|
|
74bd1cf911 | ||
|
|
ff2a9f95c4 | ||
|
|
9921c3332e | ||
|
|
f2927290f5 | ||
|
|
1551d9b6f3 | ||
|
|
5e985f4a84 | ||
|
|
2c115eda94 | ||
|
|
ebe45c6a47 | ||
|
|
82fc334be7 | ||
|
|
cd56163b1b | ||
|
|
ccc2c1fd4c | ||
|
|
08c9124858 | ||
|
|
1bdbfe22c1 | ||
|
|
9e7c304556 | ||
|
|
bc4f88aee9 | ||
|
|
400d7d0bea | ||
|
|
ac4c75d3c3 | ||
|
|
507b0a5aee | ||
|
|
f5db888bbd | ||
|
|
d7dee39fab | ||
|
|
3412786282 | ||
|
|
861cbc7759 | ||
|
|
fefa3852f7 | ||
|
|
68b68d6ce7 | ||
|
|
47b5895404 | ||
|
|
e18e4bb0e1 | ||
|
|
6e4e28fa00 | ||
|
|
e326e3d809 | ||
|
|
fbc4c2d054 | ||
|
|
3078c467d0 | ||
|
|
52927ff06b | ||
|
|
08e8b9d2a4 | ||
|
|
f1448ed947 | ||
|
|
c0dcfdd835 | ||
|
|
70295b7a6b | ||
|
|
480942339f | ||
|
|
6ad0b4299a | ||
|
|
0adec35848 | ||
|
|
0fe4a3671a | ||
|
|
363d115be8 | ||
|
|
7595827d29 | ||
|
|
117724b0ae | ||
|
|
a5d221dc13 | ||
|
|
cbebfe5e99 | ||
|
|
0c768979d4 | ||
|
|
53ef42adfa | ||
|
|
0954f50912 | ||
|
|
5d5fc673b1 | ||
|
|
2bd0c89588 | ||
|
|
f03a139e0e | ||
|
|
f9b5a626cf | ||
|
|
dc336569ff | ||
|
|
0ec6a17ac7 | ||
|
|
129156a1c1 | ||
|
|
33ada1d994 | ||
|
|
0afcae4504 | ||
|
|
93e60471de | ||
|
|
5437ab477f | ||
|
|
3b54485127 | ||
|
|
c3b2443ab5 | ||
|
|
2fdafc5c85 | ||
|
|
09c2264de4 | ||
|
|
c3be151d40 | ||
|
|
abb47751c8 | ||
|
|
1d517103b3 | ||
|
|
fe5d1bc36e | ||
|
|
b589103b05 | ||
|
|
cc76125b1c | ||
|
|
f3bcc4d554 | ||
|
|
2dd6dd201c | ||
|
|
cb0214ca2f | ||
|
|
5a5a1487a3 | ||
|
|
de408e4d58 | ||
|
|
a1bf26e7e8 | ||
|
|
3bb7db10fb | ||
|
|
8a68a1bff5 | ||
|
|
01e6f58463 | ||
|
|
7498bc469f | ||
|
|
23ba77c107 | ||
|
|
1a1d1c1840 | ||
|
|
b019da2a8c | ||
|
|
60becf0a09 | ||
|
|
974b215610 | ||
|
|
8e191ae075 | ||
|
|
b189ac027b | ||
|
|
6cbc8bd58f | ||
|
|
49f077aca0 | ||
|
|
525d1c6bf9 | ||
|
|
124b54ab04 | ||
|
|
b8b2127a5d | ||
|
|
586bd9d0cc | ||
|
|
03bb07a9c0 | ||
|
|
279a861582 | ||
|
|
82a1271281 | ||
|
|
26a6cc4735 | ||
|
|
61d70f7b4e | ||
|
|
0213096c58 | ||
|
|
dc47a08988 | ||
|
|
83574dfeb1 | ||
|
|
87f44d7a88 | ||
|
|
cda6a4c420 | ||
|
|
4093228e61 | ||
|
|
442ae960cf | ||
|
|
e7f7077e96 | ||
|
|
e5620e17e0 | ||
|
|
93b0996794 | ||
|
|
1d70be8060 | ||
|
|
eab98ec0e4 | ||
|
|
861b7b78cd | ||
|
|
6eacc82dcd | ||
|
|
1afbbfef78 | ||
|
|
7cf0367361 | ||
|
|
0b0c92eb09 | ||
|
|
34d51b66aa | ||
|
|
cda9b1df65 | ||
|
|
509f11561a | ||
|
|
13adbd5466 | ||
|
|
26985e01a2 | ||
|
|
deba8ac910 | ||
|
|
1ebe4099d6 | ||
|
|
c6de7ddebd | ||
|
|
8860d1e39a | ||
|
|
2413424c8a | ||
|
|
9b25193d4e | ||
|
|
70844610d8 | ||
|
|
e4cfd674ee | ||
|
|
776a93dc73 | ||
|
|
514b0e7f30 | ||
|
|
4a00d82921 | ||
|
|
fdd75e2866 | ||
|
|
b2860c1047 | ||
|
|
1f172b4632 | ||
|
|
d40c59eee9 | ||
|
|
f8d0616acd | ||
|
|
31f23024a4 | ||
|
|
6d2b72e079 | ||
|
|
b76626ac8d | ||
|
|
579969a9e1 | ||
|
|
2f0bf19d00 | ||
|
|
ef26049c53 | ||
|
|
a73dee83b3 |
303 changed files with 9973 additions and 2121 deletions
|
|
@ -3576,6 +3576,18 @@ resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_
|
|||
resharper_xaml_x_key_attribute_disallowed_highlighting=error
|
||||
resharper_xml_doc_comment_syntax_problem_highlighting=warning
|
||||
resharper_xunit_xunit_test_with_console_output_highlighting=warning
|
||||
csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion
|
||||
csharp_style_expression_bodied_methods = true:silent
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_expression_bodied_constructors = true:silent
|
||||
csharp_style_expression_bodied_operators = true:silent
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
|
||||
[*.{cshtml,htm,html,proto,razor}]
|
||||
indent_style=tab
|
||||
|
|
|
|||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '8.x.x'
|
||||
dotnet-version: '9.x.x'
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
|
|
|
|||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -15,12 +15,12 @@ jobs:
|
|||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '8.x.x'
|
||||
dotnet-version: '9.x.x'
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
|
||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||
- name: Build
|
||||
run: |
|
||||
|
|
|
|||
2
.github/workflows/test_release.yml
vendored
2
.github/workflows/test_release.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '8.x.x'
|
||||
dotnet-version: '9.x.x'
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
|
|
|
|||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 0b6085ce720ffb7c78cf42d4e51861f34db27744
|
||||
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 70f046830cc7cd35b3480b12b7efe94182477fbb
|
||||
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Penumbra.CrashHandler.Buffers;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Penumbra.CrashHandler.Buffers;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Penumbra.CrashHandler.Buffers;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Penumbra.CrashHandler.Buffers;
|
||||
using System;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Linux'))">$(HOME)/.xlcore/dalamud/Hooks/dev/</DalamudLibPath>
|
||||
<DalamudLibPath Condition="$(DALAMUD_HOME) != ''">$(DALAMUD_HOME)/</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
|
@ -25,4 +11,8 @@
|
|||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Use_DalamudPackager>false</Use_DalamudPackager>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
|
|
|||
13
Penumbra.CrashHandler/packages.lock.json
Normal file
13
Penumbra.CrashHandler/packages.lock.json
Normal 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 f6dff467c7dad6b1213a7d7b65d40a56450f0672
|
||||
Subproject commit d889f9ef918514a46049725052d378b441915b00
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 4eb7c118cdac5873afb97cb04719602f061f03b7
|
||||
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793
|
||||
55
Penumbra.sln
55
Penumbra.sln
|
|
@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.github\workflows\build.yml = .github\workflows\build.yml
|
||||
Penumbra\Penumbra.json = Penumbra\Penumbra.json
|
||||
.github\workflows\release.yml = .github\workflows\release.yml
|
||||
repo.json = repo.json
|
||||
.github\workflows\test_release.yml = .github\workflows\test_release.yml
|
||||
|
|
@ -41,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
|
|||
schemas\structs\group_single.json = schemas\structs\group_single.json
|
||||
schemas\structs\manipulation.json = schemas\structs\manipulation.json
|
||||
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
|
||||
schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
|
||||
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
|
||||
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
|
||||
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
|
||||
|
|
@ -49,39 +51,40 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
|
|||
schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
|
||||
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
|
||||
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
|
||||
schemas\structs\meta_shp.json = schemas\structs\meta_shp.json
|
||||
schemas\structs\option.json = schemas\structs\option.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
|
||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64
|
||||
{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64
|
||||
{5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64
|
||||
{EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
|
|
@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
|
|||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly DrawObjectState _drawObjectState;
|
||||
private readonly CutsceneService _cutsceneService;
|
||||
private readonly ResourceLoader _resourceLoader;
|
||||
|
||||
public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
|
||||
ResourceLoader resourceLoader)
|
||||
ResourceLoader resourceLoader, DrawObjectState drawObjectState)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_collectionResolver = collectionResolver;
|
||||
_cutsceneService = cutsceneService;
|
||||
_resourceLoader = resourceLoader;
|
||||
_drawObjectState = drawObjectState;
|
||||
_resourceLoader.ResourceLoaded += OnResourceLoaded;
|
||||
_resourceLoader.PapRequested += OnPapRequested;
|
||||
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
|
||||
|
|
@ -67,6 +69,30 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
|
|||
public int GetCutsceneParentIndex(int actorIdx)
|
||||
=> _cutsceneService.GetParentIndex(actorIdx);
|
||||
|
||||
public Func<int, int> GetCutsceneParentIndexFunc()
|
||||
{
|
||||
var weakRef = new WeakReference<CutsceneService>(_cutsceneService);
|
||||
return idx =>
|
||||
{
|
||||
if (!weakRef.TryGetTarget(out var c))
|
||||
throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed.");
|
||||
|
||||
return c.GetParentIndex(idx);
|
||||
};
|
||||
}
|
||||
|
||||
public Func<nint, nint> GetGameObjectFromDrawObjectFunc()
|
||||
{
|
||||
var weakRef = new WeakReference<DrawObjectState>(_drawObjectState);
|
||||
return model =>
|
||||
{
|
||||
if (!weakRef.TryGetTarget(out var c))
|
||||
throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed.");
|
||||
|
||||
return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero;
|
||||
};
|
||||
}
|
||||
|
||||
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
|
||||
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
|
||||
? PenumbraApiEc.Success
|
||||
|
|
|
|||
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Penumbra.Api.Api;
|
||||
|
||||
public static class IdentityChecker
|
||||
{
|
||||
public static bool Check(string identity)
|
||||
=> true;
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
}
|
||||
|
||||
internal static string CompressMetaManipulations(ModCollection collection)
|
||||
=> CompressMetaManipulationsV0(collection);
|
||||
=> CompressMetaManipulationsV1(collection);
|
||||
|
||||
private static string CompressMetaManipulationsV0(ModCollection collection)
|
||||
{
|
||||
|
|
@ -66,6 +66,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair<ShpIdentifier, ShpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair<AtrIdentifier, AtrEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
}
|
||||
|
||||
return Functions.ToCompressedBase64(array, 0);
|
||||
|
|
@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
}
|
||||
|
||||
WriteCache(zipStream, cache.Atch);
|
||||
WriteCache(zipStream, cache.Shp);
|
||||
WriteCache(zipStream, cache.Atr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +144,86 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
}
|
||||
}
|
||||
|
||||
public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8);
|
||||
public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8);
|
||||
public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P';
|
||||
public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8);
|
||||
public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8);
|
||||
public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8);
|
||||
public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P';
|
||||
public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H';
|
||||
public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8);
|
||||
public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8);
|
||||
|
||||
private static unsafe string CompressMetaManipulationsV2(ModCollection? collection)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
ms.Capacity = 1024;
|
||||
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
|
||||
{
|
||||
zipStream.Write((byte)2);
|
||||
zipStream.Write("META0002"u8);
|
||||
if (collection?.MetaCache is { } cache)
|
||||
{
|
||||
WriteCache(zipStream, cache.Imc, ImcKey);
|
||||
WriteCache(zipStream, cache.Eqp, EqpKey);
|
||||
WriteCache(zipStream, cache.Eqdp, EqdpKey);
|
||||
WriteCache(zipStream, cache.Est, EstKey);
|
||||
WriteCache(zipStream, cache.Rsp, RspKey);
|
||||
WriteCache(zipStream, cache.Gmp, GmpKey);
|
||||
cache.GlobalEqp.EnterReadLock();
|
||||
|
||||
try
|
||||
{
|
||||
if (cache.GlobalEqp.Count > 0)
|
||||
{
|
||||
zipStream.Write(GeqpKey);
|
||||
zipStream.Write(cache.GlobalEqp.Count);
|
||||
foreach (var (globalEqp, _) in cache.GlobalEqp)
|
||||
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache.GlobalEqp.ExitReadLock();
|
||||
}
|
||||
|
||||
WriteCache(zipStream, cache.Atch, AtchKey);
|
||||
WriteCache(zipStream, cache.Shp, ShpKey);
|
||||
WriteCache(zipStream, cache.Atr, AtrKey);
|
||||
}
|
||||
}
|
||||
|
||||
ms.Flush();
|
||||
ms.Position = 0;
|
||||
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
|
||||
return Convert.ToBase64String(data);
|
||||
|
||||
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache, uint label)
|
||||
where TKey : unmanaged, IMetaIdentifier
|
||||
where TValue : unmanaged
|
||||
{
|
||||
metaCache.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (metaCache.Count <= 0)
|
||||
return;
|
||||
|
||||
stream.Write(label);
|
||||
stream.Write(metaCache.Count);
|
||||
foreach (var (identifier, (_, value)) in metaCache)
|
||||
{
|
||||
stream.Write(identifier);
|
||||
stream.Write(value);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
metaCache.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert manipulations from a transmitted base64 string to actual manipulations.
|
||||
/// The empty string is treated as an empty set.
|
||||
|
|
@ -170,6 +254,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
{
|
||||
case 0: return ConvertManipsV0(data, out manips);
|
||||
case 1: return ConvertManipsV1(data, out manips);
|
||||
case 2: return ConvertManipsV2(data, out manips);
|
||||
default:
|
||||
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
|
||||
manips = null;
|
||||
|
|
@ -185,6 +270,131 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
}
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV2(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (!data.StartsWith("META0002"u8))
|
||||
{
|
||||
Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix.");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
manips = new MetaDictionary();
|
||||
var r = new SpanBinaryReader(data[8..]);
|
||||
while (r.Remaining > 4)
|
||||
{
|
||||
var prefix = r.ReadUInt32();
|
||||
var count = r.Remaining > 4 ? r.ReadInt32() : 0;
|
||||
if (count is 0)
|
||||
continue;
|
||||
|
||||
switch (prefix)
|
||||
{
|
||||
case ImcKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<ImcIdentifier>();
|
||||
var value = r.Read<ImcEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EqpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqpIdentifier>();
|
||||
var value = r.Read<EqpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EqdpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqdpIdentifier>();
|
||||
var value = r.Read<EqdpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EstKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EstIdentifier>();
|
||||
var value = r.Read<EstEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case RspKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<RspIdentifier>();
|
||||
var value = r.Read<RspEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case GmpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<GmpIdentifier>();
|
||||
var value = r.Read<GmpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case GeqpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<GlobalEqpManipulation>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case AtchKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtchIdentifier>();
|
||||
var value = r.Read<AtchEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case ShpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<ShpIdentifier>();
|
||||
var value = r.Read<ShpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case AtrKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtrIdentifier>();
|
||||
var value = r.Read<AtrEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (!data.StartsWith("META0001"u8))
|
||||
|
|
@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shp and Atr was added later
|
||||
if (r.Position < r.Count)
|
||||
{
|
||||
var shpCount = r.ReadInt32();
|
||||
for (var i = 0; i < shpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<ShpIdentifier>();
|
||||
var value = r.Read<ShpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var atrCount = r.ReadInt32();
|
||||
for (var i = 0; i < atrCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtrIdentifier>();
|
||||
var value = r.Read<AtrEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Compression;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
@ -33,12 +34,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Deleted when oldDirectory != null:
|
||||
ModDeleted?.Invoke(oldDirectory.Name);
|
||||
break;
|
||||
case ModPathChangeType.Added when newDirectory != null:
|
||||
ModAdded?.Invoke(newDirectory.Name);
|
||||
break;
|
||||
case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break;
|
||||
case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break;
|
||||
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
|
||||
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
|
||||
break;
|
||||
|
|
@ -46,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
||||
{
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetModList()
|
||||
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
|
||||
|
|
@ -109,10 +108,22 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
public event Action<string>? ModAdded;
|
||||
public event Action<string, string>? ModMoved;
|
||||
|
||||
public event Action<JObject, ushort, string>? CreatingPcp
|
||||
{
|
||||
add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi);
|
||||
remove => _communicator.PcpCreation.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public event Action<JObject, string, Guid>? ParsingPcp
|
||||
{
|
||||
add => _communicator.PcpParsing.Subscribe(value!, PcpParsing.Priority.ModsApi);
|
||||
remove => _communicator.PcpParsing.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
|
||||
{
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
|| !_modFileSystem.FindLeaf(mod, out var leaf))
|
||||
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|
||||
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
|
||||
|
||||
var fullPath = leaf.FullName();
|
||||
|
|
@ -127,7 +138,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
return PenumbraApiEc.InvalidArgument;
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
|| !_modFileSystem.FindLeaf(mod, out var leaf))
|
||||
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|
||||
return PenumbraApiEc.ModMissing;
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -16,13 +16,16 @@ public class PenumbraApi(
|
|||
TemporaryApi temporary,
|
||||
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
||||
{
|
||||
public const int BreakingVersion = 5;
|
||||
public const int FeatureVersion = 13;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Valid = false;
|
||||
}
|
||||
|
||||
public (int Breaking, int Feature) ApiVersion
|
||||
=> (5, 7);
|
||||
=> (BreakingVersion, FeatureVersion);
|
||||
|
||||
public bool Valid { get; private set; } = true;
|
||||
public IPenumbraApiCollection Collection { get; } = collection;
|
||||
|
|
|
|||
|
|
@ -1,39 +1,38 @@
|
|||
using System.Collections.Frozen;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class PluginStateApi : IPenumbraApiPluginState, IApiService
|
||||
public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public PluginStateApi(Configuration config, CommunicatorService communicator)
|
||||
{
|
||||
_config = config;
|
||||
_communicator = communicator;
|
||||
}
|
||||
|
||||
public string GetModDirectory()
|
||||
=> _config.ModDirectory;
|
||||
=> config.ModDirectory;
|
||||
|
||||
public string GetConfiguration()
|
||||
=> JsonConvert.SerializeObject(_config, Formatting.Indented);
|
||||
=> JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
|
||||
public event Action<string, bool>? ModDirectoryChanged
|
||||
{
|
||||
add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
|
||||
remove => _communicator.ModDirectoryChanged.Unsubscribe(value!);
|
||||
add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
|
||||
remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public bool GetEnabledState()
|
||||
=> _config.EnableMods;
|
||||
=> config.EnableMods;
|
||||
|
||||
public event Action<bool>? EnabledChange
|
||||
{
|
||||
add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
|
||||
remove => _communicator.EnabledChanged.Unsubscribe(value!);
|
||||
add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
|
||||
remove => communicator.EnabledChanged.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public FrozenSet<string> SupportedFeatures
|
||||
=> FeatureChecker.SupportedFeatures.ToFrozenSet();
|
||||
|
||||
public string[] CheckSupportedFeatures(IEnumerable<string> requiredFeatures)
|
||||
=> requiredFeatures.Where(f => !FeatureChecker.Supported(f)).ToArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,57 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
|
||||
{
|
||||
public void RedrawObject(int gameObjectIndex, RedrawType setting)
|
||||
=> redrawService.RedrawObject(gameObjectIndex, setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting));
|
||||
}
|
||||
|
||||
public void RedrawObject(string name, RedrawType setting)
|
||||
=> redrawService.RedrawObject(name, setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting));
|
||||
}
|
||||
|
||||
public void RedrawObject(IGameObject? gameObject, RedrawType setting)
|
||||
=> redrawService.RedrawObject(gameObject, setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting));
|
||||
}
|
||||
|
||||
public void RedrawAll(RedrawType setting)
|
||||
=> redrawService.RedrawAll(setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
|
||||
}
|
||||
|
||||
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
|
||||
{
|
||||
|
||||
if (!collections.Storage.ById(collectionId, out var collection))
|
||||
collection = ModCollection.Empty;
|
||||
framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var actor in objects.Objects)
|
||||
{
|
||||
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
|
||||
if (collection == modCollection)
|
||||
{
|
||||
redrawService.RedrawObject(actor.ObjectIndex, setting);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public event GameObjectRedrawnDelegate? GameObjectRedrawn
|
||||
{
|
||||
add => redrawService.GameObjectRedrawn += value;
|
||||
remove => redrawService.GameObjectRedrawn -= value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,16 @@ public class TemporaryApi(
|
|||
ApiHelpers apiHelpers,
|
||||
ModManager modManager) : IPenumbraApiTemporary, IApiService
|
||||
{
|
||||
public Guid CreateTemporaryCollection(string name)
|
||||
=> tempCollections.CreateTemporaryCollection(name);
|
||||
public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
|
||||
{
|
||||
if (!IdentityChecker.Check(identity))
|
||||
return (PenumbraApiEc.InvalidCredentials, Guid.Empty);
|
||||
|
||||
var collection = tempCollections.CreateTemporaryCollection(name);
|
||||
if (collection == Guid.Empty)
|
||||
return (PenumbraApiEc.UnknownError, collection);
|
||||
return (PenumbraApiEc.Success, collection);
|
||||
}
|
||||
|
||||
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
|
||||
=> tempCollections.RemoveTemporaryCollection(collectionId)
|
||||
|
|
|
|||
|
|
@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
|
|||
public void CloseMainWindow()
|
||||
=> _configWindow.IsOpen = false;
|
||||
|
||||
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data)
|
||||
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data)
|
||||
{
|
||||
if (ChangedItemClicked == null)
|
||||
return;
|
||||
|
||||
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
|
||||
var (type, id) = data.ToApiObject();
|
||||
ChangedItemClicked.Invoke(button, type, id);
|
||||
}
|
||||
|
||||
private void OnChangedItemHover(IIdentifiedObjectData? data)
|
||||
private void OnChangedItemHover(IIdentifiedObjectData data)
|
||||
{
|
||||
if (ChangedItemTooltip == null)
|
||||
return;
|
||||
|
||||
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
|
||||
var (type, id) = data.ToApiObject();
|
||||
ChangedItemTooltip.Invoke(type, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using EmbedIO;
|
||||
using EmbedIO.Routing;
|
||||
using EmbedIO.WebApi;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
|
|
@ -12,23 +14,28 @@ public class HttpApi : IDisposable, IApiService
|
|||
private partial class Controller : WebApiController
|
||||
{
|
||||
// @formatter:off
|
||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll();
|
||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
|
||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
|
||||
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public const string Prefix = "http://localhost:42069/";
|
||||
|
||||
private readonly IPenumbraApi _api;
|
||||
private readonly IFramework _framework;
|
||||
private WebServer? _server;
|
||||
|
||||
public HttpApi(Configuration config, IPenumbraApi api)
|
||||
public HttpApi(Configuration config, IPenumbraApi api, IFramework framework)
|
||||
{
|
||||
_api = api;
|
||||
_api = api;
|
||||
_framework = framework;
|
||||
if (config.EnableHttpApi)
|
||||
CreateWebServer();
|
||||
}
|
||||
|
|
@ -44,7 +51,7 @@ public class HttpApi : IDisposable, IApiService
|
|||
.WithUrlPrefix(Prefix)
|
||||
.WithMode(HttpListenerMode.EmbedIO))
|
||||
.WithCors(Prefix)
|
||||
.WithWebApi("/api", m => m.WithController(() => new Controller(_api)));
|
||||
.WithWebApi("/api", m => m.WithController(() => new Controller(_api, _framework)));
|
||||
|
||||
_server.StateChanged += (_, e) => Penumbra.Log.Information($"WebServer New State - {e.NewState}");
|
||||
_server.RunAsync();
|
||||
|
|
@ -59,60 +66,96 @@ public class HttpApi : IDisposable, IApiService
|
|||
public void Dispose()
|
||||
=> ShutdownWebServer();
|
||||
|
||||
private partial class Controller
|
||||
private partial class Controller(IPenumbraApi api, IFramework framework)
|
||||
{
|
||||
private readonly IPenumbraApi _api;
|
||||
|
||||
public Controller(IPenumbraApi api)
|
||||
=> _api = api;
|
||||
public partial string GetModDirectory()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
|
||||
return api.PluginState.GetModDirectory();
|
||||
}
|
||||
|
||||
public partial object? GetMods()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
|
||||
return _api.Mods.GetModList();
|
||||
return api.Mods.GetModList();
|
||||
}
|
||||
|
||||
public async partial Task Redraw()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<RedrawData>();
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}.");
|
||||
if (data.ObjectTableIndex >= 0)
|
||||
_api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type);
|
||||
else
|
||||
_api.Redraw.RedrawAll(data.Type);
|
||||
var data = await HttpContext.GetRequestDataAsync<RedrawData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] [{Environment.CurrentManagedThreadId}] {nameof(Redraw)} triggered with {data}.");
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (data.ObjectTableIndex >= 0)
|
||||
api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type);
|
||||
else
|
||||
api.Redraw.RedrawAll(data.Type);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public partial void RedrawAll()
|
||||
public async partial Task RedrawAll()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered.");
|
||||
_api.Redraw.RedrawAll(RedrawType.Redraw);
|
||||
await framework.RunOnFrameworkThread(() => { api.Redraw.RedrawAll(RedrawType.Redraw); }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async partial Task ReloadMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModReloadData>();
|
||||
var data = await HttpContext.GetRequestDataAsync<ModReloadData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(ReloadMod)} triggered with {data}.");
|
||||
// Add the mod if it is not already loaded and if the directory name is given.
|
||||
// AddMod returns Success if the mod is already loaded.
|
||||
if (data.Path.Length != 0)
|
||||
_api.Mods.AddMod(data.Path);
|
||||
api.Mods.AddMod(data.Path);
|
||||
|
||||
// Reload the mod by path or name, which will also remove no-longer existing mods.
|
||||
_api.Mods.ReloadMod(data.Path, data.Name);
|
||||
api.Mods.ReloadMod(data.Path, data.Name);
|
||||
}
|
||||
|
||||
public async partial Task InstallMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModInstallData>();
|
||||
var data = await HttpContext.GetRequestDataAsync<ModInstallData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}.");
|
||||
if (data.Path.Length != 0)
|
||||
_api.Mods.InstallMod(data.Path);
|
||||
api.Mods.InstallMod(data.Path);
|
||||
}
|
||||
|
||||
public partial void OpenWindow()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
|
||||
_api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
|
||||
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
public async partial Task FocusMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered.");
|
||||
if (data.Path.Length != 0)
|
||||
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
|
||||
}
|
||||
|
||||
public async partial Task SetModSettings()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
|
||||
if (data.Inherit.HasValue)
|
||||
{
|
||||
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
|
||||
if (data.Inherit.Value)
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.State.HasValue)
|
||||
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
|
||||
if (data.Priority.HasValue)
|
||||
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
|
||||
foreach (var (group, settings) in data.Settings ?? [])
|
||||
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private record ModReloadData(string Path, string Name)
|
||||
|
|
@ -122,6 +165,13 @@ public class HttpApi : IDisposable, IApiService
|
|||
{ }
|
||||
}
|
||||
|
||||
private record ModFocusData(string Path, string Name)
|
||||
{
|
||||
public ModFocusData()
|
||||
: this(string.Empty, string.Empty)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record ModInstallData(string Path)
|
||||
{
|
||||
public ModInstallData()
|
||||
|
|
@ -135,5 +185,19 @@ public class HttpApi : IDisposable, IApiService
|
|||
: this(string.Empty, RedrawType.Redraw, -1)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record SetModSettingsData(
|
||||
Guid? CollectionId,
|
||||
string ModPath,
|
||||
string ModName,
|
||||
bool? Inherit,
|
||||
bool? State,
|
||||
int? Priority,
|
||||
Dictionary<string, List<string>>? Settings)
|
||||
{
|
||||
public SetModSettingsData()
|
||||
: this(null, string.Empty, string.Empty, null, null, null, null)
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
Penumbra/Api/IpcLaunchingProvider.cs
Normal file
28
Penumbra/Api/IpcLaunchingProvider.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,8 @@ public sealed class IpcProviders : IDisposable, IApiService
|
|||
IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState),
|
||||
IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState),
|
||||
|
||||
IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta),
|
||||
IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta),
|
||||
|
|
@ -52,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService
|
|||
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ModMoved.Provider(pi, api.Mods),
|
||||
IpcSubscribers.CreatingPcp.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ParsingPcp.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetModPath.Provider(pi, api.Mods),
|
||||
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
|
||||
|
|
@ -78,10 +82,13 @@ public sealed class IpcProviders : IDisposable, IApiService
|
|||
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
|
||||
|
||||
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
|
||||
|
||||
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
@ -121,6 +121,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
|||
}).ToArray();
|
||||
ImGui.OpenPopup("Changed Item List");
|
||||
}
|
||||
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
|
||||
if (ImGui.Button("Redraw##ObjectCollection"))
|
||||
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
|
||||
|
||||
}
|
||||
|
||||
private void DrawChangedItemPopup()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ namespace Penumbra.Api.IpcTester;
|
|||
|
||||
public class PluginStateIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
|
||||
public readonly EventSubscriber Initialized;
|
||||
public readonly EventSubscriber Disposed;
|
||||
|
|
@ -26,6 +27,9 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
|||
private readonly List<DateTimeOffset> _initializedList = [];
|
||||
private readonly List<DateTimeOffset> _disposedList = [];
|
||||
|
||||
private string _requiredFeatureString = string.Empty;
|
||||
private string[] _requiredFeatures = [];
|
||||
|
||||
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
|
||||
private bool? _lastEnabledValue;
|
||||
|
||||
|
|
@ -48,12 +52,15 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
|||
EnabledChange.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Plugin State");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
if (ImUtf8.InputText("Required Features"u8, ref _requiredFeatureString))
|
||||
_requiredFeatures = _requiredFeatureString.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
|
@ -71,6 +78,12 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
|||
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
|
||||
ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
|
||||
|
||||
IpcTester.DrawIntro(SupportedFeatures.Label, "Supported Features");
|
||||
ImUtf8.Text(string.Join(", ", new SupportedFeatures(_pi).Invoke()));
|
||||
|
||||
IpcTester.DrawIntro(CheckSupportedFeatures.Label, "Missing Features");
|
||||
ImUtf8.Text(string.Join(", ", new CheckSupportedFeatures(_pi).Invoke(_requiredFeatures)));
|
||||
|
||||
DrawConfigPopup();
|
||||
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
|
||||
if (ImGui.Button("Get"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
@ -37,6 +38,7 @@ public class TemporaryIpcTester(
|
|||
private string _tempGamePath = "test/game/path.mtrl";
|
||||
private string _tempFilePath = "test/success.mtrl";
|
||||
private string _tempManipulation = string.Empty;
|
||||
private string _identity = string.Empty;
|
||||
private PenumbraApiEc _lastTempError;
|
||||
private int _tempActorIndex;
|
||||
private bool _forceOverwrite;
|
||||
|
|
@ -47,6 +49,7 @@ public class TemporaryIpcTester(
|
|||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128);
|
||||
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
||||
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
||||
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
||||
|
|
@ -72,7 +75,7 @@ public class TemporaryIpcTester(
|
|||
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
|
||||
if (ImGui.Button("Create##Collection"))
|
||||
{
|
||||
LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName);
|
||||
_lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
|
||||
if (_tempGuid == null)
|
||||
{
|
||||
_tempGuid = LastCreatedCollectionId;
|
||||
|
|
@ -281,7 +284,7 @@ public class TemporaryIpcTester(
|
|||
foreach (var mod in list)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Name);
|
||||
ImGui.TextUnformatted(mod.Name.Text);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Priority.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ public sealed class ModChangedItemAdapter(WeakReference<ModStorage> storage)
|
|||
: throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed.");
|
||||
}
|
||||
|
||||
private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData?> data) : IReadOnlyDictionary<string, object?>
|
||||
private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData> data) : IReadOnlyDictionary<string, object?>
|
||||
{
|
||||
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
|
||||
=> data.Select(d => new KeyValuePair<string, object?>(d.Key, d.Value?.ToInternalObject())).GetEnumerator();
|
||||
|
|
|
|||
57
Penumbra/ChangedItemMode.cs
Normal file
57
Penumbra/ChangedItemMode.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Text;
|
||||
|
||||
namespace Penumbra;
|
||||
|
||||
public enum ChangedItemMode
|
||||
{
|
||||
GroupedCollapsed,
|
||||
GroupedExpanded,
|
||||
Alphabetical,
|
||||
}
|
||||
|
||||
public static class ChangedItemModeExtensions
|
||||
{
|
||||
public static ReadOnlySpan<byte> ToName(this ChangedItemMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
ChangedItemMode.GroupedCollapsed => "Grouped (Collapsed)"u8,
|
||||
ChangedItemMode.GroupedExpanded => "Grouped (Expanded)"u8,
|
||||
ChangedItemMode.Alphabetical => "Alphabetical"u8,
|
||||
_ => "Error"u8,
|
||||
};
|
||||
|
||||
public static ReadOnlySpan<byte> ToTooltip(this ChangedItemMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
ChangedItemMode.GroupedCollapsed =>
|
||||
"Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item."u8,
|
||||
ChangedItemMode.GroupedExpanded =>
|
||||
"Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item."u8,
|
||||
ChangedItemMode.Alphabetical => "Display all changed items in a single list sorted alphabetically."u8,
|
||||
_ => ""u8,
|
||||
};
|
||||
|
||||
public static bool DrawCombo(ReadOnlySpan<byte> label, ChangedItemMode value, float width, Action<ChangedItemMode> setter)
|
||||
{
|
||||
ImGui.SetNextItemWidth(width);
|
||||
using var combo = ImUtf8.Combo(label, value.ToName());
|
||||
if (!combo)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
foreach (var newValue in Enum.GetValues<ChangedItemMode>())
|
||||
{
|
||||
var selected = ImUtf8.Selectable(newValue.ToName(), newValue == value);
|
||||
if (selected)
|
||||
{
|
||||
ret = true;
|
||||
setter(newValue);
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip(newValue.ToTooltip());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
65
Penumbra/Collections/Cache/AtrCache.cs
Normal file
65
Penumbra/Collections/Cache/AtrCache.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -8,6 +7,7 @@ using Penumbra.Mods.Editor;
|
|||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.GameData.Data;
|
||||
using OtterGui.Extensions;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ public sealed class CollectionCache : IDisposable
|
|||
private readonly CollectionCacheManager _manager;
|
||||
private readonly ModCollection _collection;
|
||||
public readonly CollectionModData ModData = new();
|
||||
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> _changedItems = [];
|
||||
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> _changedItems = [];
|
||||
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
|
||||
public readonly CustomResourceCache CustomResources;
|
||||
public readonly MetaCache Meta;
|
||||
|
|
@ -43,7 +43,7 @@ public sealed class CollectionCache : IDisposable
|
|||
private int _changedItemsSaveCounter = -1;
|
||||
|
||||
// Obtain currently changed items. Computes them if they haven't been computed before.
|
||||
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
|
||||
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -245,6 +245,10 @@ public sealed class CollectionCache : IDisposable
|
|||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atch)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Shp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atr)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var identifier in files.Manipulations.GlobalEqp)
|
||||
AddManipulation(mod, identifier, null!);
|
||||
}
|
||||
|
|
@ -441,7 +445,7 @@ public sealed class CollectionCache : IDisposable
|
|||
// Skip IMCs because they would result in far too many false-positive items,
|
||||
// since they are per set instead of per item-slot/item/variant.
|
||||
var identifier = _manager.MetaFileManager.Identifier;
|
||||
var items = new SortedList<string, IIdentifiedObjectData?>(512);
|
||||
var items = new SortedList<string, IIdentifiedObjectData>(512);
|
||||
|
||||
void AddItems(IMod mod)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection)
|
|||
? entry.HasFlag(EqpEntry.BodyHideGlovesL)
|
||||
: entry.HasFlag(EqpEntry.BodyHideGlovesM);
|
||||
return testFlag
|
||||
? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS
|
||||
: entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS);
|
||||
? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS
|
||||
: entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
|
||||
private bool _doNotHideVieraHats;
|
||||
private bool _doNotHideHrothgarHats;
|
||||
private bool _hideAuRaHorns;
|
||||
private bool _hideVieraEars;
|
||||
private bool _hideMiqoteEars;
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
|
|
@ -26,6 +29,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
_doNotHideRingR.Clear();
|
||||
_doNotHideHrothgarHats = false;
|
||||
_doNotHideVieraHats = false;
|
||||
_hideAuRaHorns = false;
|
||||
_hideVieraEars = false;
|
||||
_hideMiqoteEars = false;
|
||||
}
|
||||
|
||||
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
|
||||
|
|
@ -39,8 +45,20 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
if (_doNotHideHrothgarHats)
|
||||
original |= EqpEntry.HeadShowHrothgarHat;
|
||||
|
||||
if (_hideAuRaHorns)
|
||||
original &= ~EqpEntry.HeadShowEarAuRa;
|
||||
|
||||
if (_hideVieraEars)
|
||||
original &= ~EqpEntry.HeadShowEarViera;
|
||||
|
||||
if (_hideMiqoteEars)
|
||||
original &= ~EqpEntry.HeadShowEarMiqote;
|
||||
|
||||
if (_doNotHideEarrings.Contains(armor[5].Set))
|
||||
original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura;
|
||||
original |= EqpEntry.HeadShowEarringsHyurRoe
|
||||
| EqpEntry.HeadShowEarringsLalaElezen
|
||||
| EqpEntry.HeadShowEarringsMiqoHrothViera
|
||||
| EqpEntry.HeadShowEarringsAura;
|
||||
|
||||
if (_doNotHideNecklace.Contains(armor[6].Set))
|
||||
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
||||
|
|
@ -53,6 +71,7 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
|
||||
if (_doNotHideRingL.Contains(armor[9].Set))
|
||||
original |= EqpEntry.HandShowRingL;
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +90,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
|
||||
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
|
||||
GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true),
|
||||
GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true),
|
||||
GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true),
|
||||
_ => false,
|
||||
};
|
||||
return true;
|
||||
|
|
@ -90,6 +112,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
|
||||
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
|
||||
GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false),
|
||||
GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false),
|
||||
GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false),
|
||||
_ => false,
|
||||
};
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
public readonly RspCache Rsp = new(manager, collection);
|
||||
public readonly ImcCache Imc = new(manager, collection);
|
||||
public readonly AtchCache Atch = new(manager, collection);
|
||||
public readonly ShpCache Shp = new(manager, collection);
|
||||
public readonly AtrCache Atr = new(manager, collection);
|
||||
public readonly GlobalEqpCache GlobalEqp = new();
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public int Count
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count;
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
|
||||
|
||||
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
|
||||
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
|
||||
|
|
@ -30,6 +32,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
|
||||
|
||||
public void Reset()
|
||||
|
|
@ -41,6 +45,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Rsp.Reset();
|
||||
Imc.Reset();
|
||||
Atch.Reset();
|
||||
Shp.Reset();
|
||||
Atr.Reset();
|
||||
GlobalEqp.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +63,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Rsp.Dispose();
|
||||
Imc.Dispose();
|
||||
Atch.Dispose();
|
||||
Shp.Dispose();
|
||||
Atr.Dispose();
|
||||
}
|
||||
|
||||
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
||||
|
|
@ -71,6 +79,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
|
||||
_ => false,
|
||||
};
|
||||
|
|
@ -92,6 +102,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i => Imc.RevertMod(i, out mod),
|
||||
RspIdentifier i => Rsp.RevertMod(i, out mod),
|
||||
AtchIdentifier i => Atch.RevertMod(i, out mod),
|
||||
ShpIdentifier i => Shp.RevertMod(i, out mod),
|
||||
AtrIdentifier i => Atr.RevertMod(i, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
|
||||
_ => (mod = null) != null,
|
||||
};
|
||||
|
|
@ -108,6 +120,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
|
||||
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
|
||||
AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
|
||||
ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e),
|
||||
AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e),
|
||||
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
|
||||
_ => false,
|
||||
};
|
||||
|
|
|
|||
181
Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
Normal file
181
Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
106
Penumbra/Collections/Cache/ShpCache.cs
Normal file
106
Penumbra/Collections/Cache/ShpCache.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable
|
|||
return;
|
||||
|
||||
var collection = _resolver.PlayerCollection();
|
||||
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
||||
_collections.SetCollection(collection, CollectionType.Current);
|
||||
if (collection.Identity.Id == Guid.Empty)
|
||||
{
|
||||
Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
||||
_collections.SetCollection(collection, CollectionType.Current);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Mods;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Communication;
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ public partial class ModCollection
|
|||
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
|
||||
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
|
||||
|
||||
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
|
||||
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)>();
|
||||
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
|
||||
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)>();
|
||||
|
||||
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
|
||||
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
|
|
@ -75,20 +75,21 @@ public class CommandHandler : IDisposable, IApiService
|
|||
|
||||
_ = argumentList[0].ToLowerInvariant() switch
|
||||
{
|
||||
"window" => ToggleWindow(arguments),
|
||||
"enable" => SetPenumbraState(arguments, true),
|
||||
"disable" => SetPenumbraState(arguments, false),
|
||||
"toggle" => SetPenumbraState(arguments, null),
|
||||
"reload" => Reload(arguments),
|
||||
"redraw" => Redraw(arguments),
|
||||
"lockui" => SetUiLockState(arguments),
|
||||
"size" => SetUiMinimumSize(arguments),
|
||||
"debug" => SetDebug(arguments),
|
||||
"collection" => SetCollection(arguments),
|
||||
"mod" => SetMod(arguments),
|
||||
"bulktag" => SetTag(arguments),
|
||||
"knowledge" => HandleKnowledge(arguments),
|
||||
_ => PrintHelp(argumentList[0]),
|
||||
"window" => ToggleWindow(arguments),
|
||||
"enable" => SetPenumbraState(arguments, true),
|
||||
"disable" => SetPenumbraState(arguments, false),
|
||||
"toggle" => SetPenumbraState(arguments, null),
|
||||
"reload" => Reload(arguments),
|
||||
"redraw" => Redraw(arguments),
|
||||
"lockui" => SetUiLockState(arguments),
|
||||
"size" => SetUiMinimumSize(arguments),
|
||||
"debug" => SetDebug(arguments),
|
||||
"collection" => SetCollection(arguments),
|
||||
"mod" => SetMod(arguments),
|
||||
"bulktag" => SetTag(arguments),
|
||||
"clearsettings" => ClearSettings(arguments),
|
||||
"knowledge" => HandleKnowledge(arguments),
|
||||
_ => PrintHelp(argumentList[0]),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +127,21 @@ public class CommandHandler : IDisposable, IApiService
|
|||
_chat.Print(new SeStringBuilder()
|
||||
.AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.")
|
||||
.BuiltString);
|
||||
_chat.Print(new SeStringBuilder()
|
||||
.AddCommand("clearsettings",
|
||||
"Clear all temporary settings applied manually through Penumbra in the current or all collections. Use with 'all' parameter for all.")
|
||||
.BuiltString);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ClearSettings(string arguments)
|
||||
{
|
||||
if (arguments.Trim().ToLowerInvariant() is "all")
|
||||
foreach (var collection in _collectionManager.Storage)
|
||||
_collectionEditor.ClearTemporarySettings(collection);
|
||||
else
|
||||
_collectionEditor.ClearTemporarySettings(_collectionManager.Active.Current);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -416,7 +432,8 @@ public class CommandHandler : IDisposable, IApiService
|
|||
var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (split2.Length < 2)
|
||||
{
|
||||
_chat.Print("Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options.");
|
||||
_chat.Print(
|
||||
"Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Penumbra.Communication;
|
|||
/// <item>Parameter is the clicked object data if any. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
|
||||
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData, ChangedItemClick.Priority>(nameof(ChangedItemClick))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Penumbra.Communication;
|
|||
/// <item>Parameter is the hovered object data if any. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
|
||||
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData, ChangedItemHover.Priority>(nameof(ChangedItemHover))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Penumbra.Api;
|
|||
using Penumbra.Api.Api;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
|
|
@ -20,11 +21,14 @@ public sealed class ModPathChanged()
|
|||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="PcpService.OnModPathChange"/>
|
||||
PcpService = int.MinValue,
|
||||
|
||||
/// <seealso cref="ModsApi.OnModPathChange"/>
|
||||
ApiMods = int.MinValue,
|
||||
ApiMods = int.MinValue + 1,
|
||||
|
||||
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
|
||||
ApiModSettings = int.MinValue,
|
||||
ApiModSettings = int.MinValue + 1,
|
||||
|
||||
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
||||
EphemeralConfig = -500,
|
||||
|
|
|
|||
21
Penumbra/Communication/PcpCreation.cs
Normal file
21
Penumbra/Communication/PcpCreation.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
21
Penumbra/Communication/PcpParsing.cs
Normal file
21
Penumbra/Communication/PcpParsing.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using Dalamud.Configuration;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Widgets;
|
||||
|
|
@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
|||
|
||||
namespace Penumbra;
|
||||
|
||||
public record PcpSettings
|
||||
{
|
||||
public bool CreateCollection { get; set; } = true;
|
||||
public bool AssignCollection { get; set; } = true;
|
||||
public bool AllowIpc { get; set; } = true;
|
||||
public bool DisableHandling { get; set; } = false;
|
||||
public string FolderName { get; set; } = "PCP";
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration, ISavable, IService
|
||||
{
|
||||
|
|
@ -39,15 +48,12 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
public bool EnableMods
|
||||
{
|
||||
get => _enableMods;
|
||||
set
|
||||
{
|
||||
_enableMods = value;
|
||||
ModsEnabled?.Invoke(value);
|
||||
}
|
||||
set => SetField(ref _enableMods, value, ModsEnabled);
|
||||
}
|
||||
|
||||
public string ModDirectory { get; set; } = string.Empty;
|
||||
public string ExportDirectory { get; set; } = string.Empty;
|
||||
public string WatchDirectory { get; set; } = string.Empty;
|
||||
|
||||
public bool? UseCrashHandler { get; set; } = null;
|
||||
public bool OpenWindowAtStart { get; set; } = false;
|
||||
|
|
@ -58,21 +64,26 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
|
||||
public bool AutoSelectCollection { get; set; } = false;
|
||||
|
||||
public bool ShowModsInLobby { get; set; } = true;
|
||||
public bool UseCharacterCollectionInMainWindow { get; set; } = true;
|
||||
public bool UseCharacterCollectionsInCards { get; set; } = true;
|
||||
public bool UseCharacterCollectionInInspect { get; set; } = true;
|
||||
public bool UseCharacterCollectionInTryOn { get; set; } = true;
|
||||
public bool UseOwnerNameForCharacterCollection { get; set; } = true;
|
||||
public bool UseNoModsInInspect { get; set; } = false;
|
||||
public bool HideChangedItemFilters { get; set; } = false;
|
||||
public bool ReplaceNonAsciiOnImport { get; set; } = false;
|
||||
public bool HidePrioritiesInSelector { get; set; } = false;
|
||||
public bool HideRedrawBar { get; set; } = false;
|
||||
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
||||
public bool DefaultTemporaryMode { get; set; } = false;
|
||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
||||
public bool ShowModsInLobby { get; set; } = true;
|
||||
public bool UseCharacterCollectionInMainWindow { get; set; } = true;
|
||||
public bool UseCharacterCollectionsInCards { get; set; } = true;
|
||||
public bool UseCharacterCollectionInInspect { get; set; } = true;
|
||||
public bool UseCharacterCollectionInTryOn { get; set; } = true;
|
||||
public bool UseOwnerNameForCharacterCollection { get; set; } = true;
|
||||
public bool UseNoModsInInspect { get; set; } = false;
|
||||
public bool HideChangedItemFilters { get; set; } = false;
|
||||
public bool ReplaceNonAsciiOnImport { get; set; } = false;
|
||||
public bool HidePrioritiesInSelector { get; set; } = false;
|
||||
public bool HideRedrawBar { get; set; } = false;
|
||||
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
||||
public bool DefaultTemporaryMode { get; set; } = false;
|
||||
public bool EnableDirectoryWatch { get; set; } = false;
|
||||
public bool EnableAutomaticModImport { get; set; } = false;
|
||||
public bool EnableCustomShapes { get; set; } = true;
|
||||
public PcpSettings PcpSettings = new();
|
||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
||||
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
||||
|
||||
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);
|
||||
|
||||
|
|
@ -87,9 +98,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
[JsonProperty(Order = int.MaxValue)]
|
||||
public ISortMode<Mod> SortMode = ISortMode<Mod>.FoldersFirst;
|
||||
|
||||
public bool ScaleModSelector { get; set; } = false;
|
||||
public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize;
|
||||
public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize;
|
||||
public bool OpenFoldersByDefault { get; set; } = false;
|
||||
public int SingleGroupRadioMax { get; set; } = 2;
|
||||
public string DefaultImportFolder { get; set; } = string.Empty;
|
||||
|
|
@ -97,6 +105,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
public string QuickMoveFolder2 { get; set; } = string.Empty;
|
||||
public string QuickMoveFolder3 { get; set; } = string.Empty;
|
||||
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
|
||||
public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control);
|
||||
public bool PrintSuccessfulCommandsToChat { get; set; } = true;
|
||||
public bool AutoDeduplicateOnImport { get; set; } = true;
|
||||
public bool AutoReduplicateUiOnImport { get; set; } = true;
|
||||
|
|
@ -217,4 +226,45 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(jWriter, this);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool SetField<T>(ref T field, T value, Action<T, T>? @event, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(value))
|
||||
return false;
|
||||
|
||||
var oldValue = field;
|
||||
field = value;
|
||||
try
|
||||
{
|
||||
@event?.Invoke(oldValue, field);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} from {oldValue} to {field}:\n{ex}");
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool SetField<T>(ref T field, T value, Action<T>? @event, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(value))
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
try
|
||||
{
|
||||
@event?.Invoke(field);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} to {field}:\n{ex}");
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@ namespace Penumbra;
|
|||
|
||||
public class DebugConfiguration
|
||||
{
|
||||
public static bool WriteImcBytesToLog = false;
|
||||
public static bool WriteImcBytesToLog = false;
|
||||
public static bool UseSkinMaterialProcessing = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.FileSystem.Selector;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Communication;
|
||||
|
|
@ -23,6 +24,10 @@ public class EphemeralConfig : ISavable, IDisposable, IService
|
|||
[JsonIgnore]
|
||||
private readonly ModPathChanged _modPathChanged;
|
||||
|
||||
public float CurrentModSelectorWidth { get; set; } = 200f;
|
||||
public float ModSelectorMinimumScale { get; set; } = 0.1f;
|
||||
public float ModSelectorMaximumScale { get; set; } = 0.5f;
|
||||
|
||||
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
||||
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
|
||||
public bool DebugSeparateWindow { get; set; } = false;
|
||||
|
|
@ -41,6 +46,7 @@ public class EphemeralConfig : ISavable, IDisposable, IService
|
|||
public string LastModPath { get; set; } = string.Empty;
|
||||
public bool AdvancedEditingOpen { get; set; } = false;
|
||||
public bool ForceRedrawOnFileChange { get; set; } = false;
|
||||
public bool IncognitoMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Load the current configuration.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
using Penumbra.UI.AdvancedWindow.Materials;
|
||||
using SharpGLTF.Materials;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Advanced;
|
||||
|
|
@ -140,13 +141,13 @@ public class MaterialExporter
|
|||
|
||||
// Lerp between table row values to fetch final pixel values for each subtexture.
|
||||
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend);
|
||||
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
|
||||
baseColorSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedDiffuse), 1));
|
||||
|
||||
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend);
|
||||
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1));
|
||||
specularSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedSpecularColor), 1));
|
||||
|
||||
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend);
|
||||
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
|
||||
emissiveSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedEmissive), 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -288,7 +289,7 @@ public class MaterialExporter
|
|||
const uint valueFace = 0x6E5B8F10;
|
||||
|
||||
var isFace = material.Mtrl.ShaderPackage.ShaderKeys
|
||||
.Any(key => key is { Category: categoryHairType, Value: valueFace });
|
||||
.Any(key => key is { Key: categoryHairType, Value: valueFace });
|
||||
|
||||
var normal = material.Textures[TextureUsage.SamplerNormal];
|
||||
var mask = material.Textures[TextureUsage.SamplerMask];
|
||||
|
|
@ -363,7 +364,7 @@ public class MaterialExporter
|
|||
|
||||
// Face is the default for the skin shader, so a lack of skin type category is also correct.
|
||||
var isFace = !material.Mtrl.ShaderPackage.ShaderKeys
|
||||
.Any(key => key.Category == categorySkinType && key.Value != valueFace);
|
||||
.Any(key => key.Key == categorySkinType && key.Value != valueFace);
|
||||
|
||||
// TODO: There's more nuance to skin than this, but this should be enough for a baseline reference.
|
||||
// TODO: Specular?
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ using System.Collections.Immutable;
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Lumina.Extensions;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.ModelStructs;
|
||||
using SharpGLTF.Geometry;
|
||||
using SharpGLTF.Geometry.VertexTypes;
|
||||
using SharpGLTF.IO;
|
||||
using SharpGLTF.Materials;
|
||||
using SharpGLTF.Scenes;
|
||||
|
||||
|
|
@ -84,9 +83,12 @@ public class MeshExporter
|
|||
_boneIndexMap = BuildBoneIndexMap(skeleton.Value);
|
||||
|
||||
var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements
|
||||
.GroupBy(ele => (MdlFile.VertexUsage)ele.Usage, ele => ele)
|
||||
.ToImmutableDictionary(
|
||||
element => (MdlFile.VertexUsage)element.Usage,
|
||||
element => (MdlFile.VertexType)element.Type
|
||||
g => g.Key,
|
||||
g => g.OrderBy(ele => ele.UsageIndex) // OrderBy UsageIndex is probably unnecessary as they're probably already be in order
|
||||
.Select(ele => (MdlFile.VertexType)ele.Type)
|
||||
.ToList()
|
||||
);
|
||||
|
||||
_geometryType = GetGeometryType(usages);
|
||||
|
|
@ -112,6 +114,7 @@ public class MeshExporter
|
|||
|
||||
var indexMap = new Dictionary<ushort, int>();
|
||||
// #TODO @ackwell maybe fix for V6 Models, I think this works fine.
|
||||
|
||||
foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex())
|
||||
{
|
||||
var boneName = _mdl.Bones[xivBoneIndex];
|
||||
|
|
@ -278,18 +281,22 @@ public class MeshExporter
|
|||
|
||||
var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements
|
||||
.OrderBy(element => element.Offset)
|
||||
.Select(element => ((MdlFile.VertexUsage)element.Usage, element))
|
||||
.ToList();
|
||||
|
||||
var vertices = new List<IVertexBuilder>();
|
||||
|
||||
var attributes = new Dictionary<MdlFile.VertexUsage, object>();
|
||||
var attributes = new Dictionary<MdlFile.VertexUsage, List<object>>();
|
||||
for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++)
|
||||
{
|
||||
attributes.Clear();
|
||||
|
||||
foreach (var (usage, element) in sortedElements)
|
||||
attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]);
|
||||
attributes = sortedElements
|
||||
.GroupBy(element => element.Usage)
|
||||
.ToDictionary(
|
||||
x => (MdlFile.VertexUsage)x.Key,
|
||||
x => x.OrderBy(ele => ele.UsageIndex) // Once again, OrderBy UsageIndex is probably unnecessary
|
||||
.Select(ele => ReadVertexAttribute((MdlFile.VertexType)ele.Type, streams[ele.Stream]))
|
||||
.ToList()
|
||||
);
|
||||
|
||||
|
||||
var vertexGeometry = BuildVertexGeometry(attributes);
|
||||
var vertexMaterial = BuildVertexMaterial(attributes);
|
||||
|
|
@ -320,7 +327,7 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
/// <summary> Get the vertex geometry type for this mesh's vertex usages. </summary>
|
||||
private Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
private Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, List<MdlFile.VertexType>> usages)
|
||||
{
|
||||
if (!usages.ContainsKey(MdlFile.VertexUsage.Position))
|
||||
throw _notifier.Exception("Mesh does not contain position vertex elements.");
|
||||
|
|
@ -335,29 +342,29 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
/// <summary> Build a geometry vertex from a vertex's attributes. </summary>
|
||||
private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
|
||||
private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary<MdlFile.VertexUsage, List<object>> attributes)
|
||||
{
|
||||
if (_geometryType == typeof(VertexPosition))
|
||||
return new VertexPosition(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position])
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position))
|
||||
);
|
||||
|
||||
if (_geometryType == typeof(VertexPositionNormal))
|
||||
return new VertexPositionNormal(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position]),
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Normal])
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))
|
||||
);
|
||||
|
||||
if (_geometryType == typeof(VertexPositionNormalTangent))
|
||||
{
|
||||
// (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range.
|
||||
// TODO: While this assumption is safe, it would be sensible to actually check.
|
||||
var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) * 2 - Vector4.One;
|
||||
|
||||
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
|
||||
|
||||
return new VertexPositionNormalTangent(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position]),
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Normal]),
|
||||
bitangent
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)),
|
||||
bitangent.SanitizeTangent()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -365,60 +372,90 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
/// <summary> Get the vertex material type for this mesh's vertex usages. </summary>
|
||||
private Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
private Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, List<MdlFile.VertexType>> usages)
|
||||
{
|
||||
var uvCount = 0;
|
||||
if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type))
|
||||
uvCount = type switch
|
||||
if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var list))
|
||||
{
|
||||
foreach (var type in list)
|
||||
{
|
||||
MdlFile.VertexType.Half2 => 1,
|
||||
MdlFile.VertexType.Half4 => 2,
|
||||
MdlFile.VertexType.Single2 => 1,
|
||||
MdlFile.VertexType.Single4 => 2,
|
||||
_ => throw _notifier.Exception($"Unexpected UV vertex type {type}."),
|
||||
};
|
||||
uvCount += type switch
|
||||
{
|
||||
MdlFile.VertexType.Half2 => 1,
|
||||
MdlFile.VertexType.Half4 => 2,
|
||||
MdlFile.VertexType.Single2 => 1,
|
||||
MdlFile.VertexType.Single4 => 2,
|
||||
_ => throw _notifier.Exception($"Unexpected UV vertex type {type}."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours);
|
||||
var nColors = colours?.Count ?? 0;
|
||||
|
||||
var materialUsages = (
|
||||
uvCount,
|
||||
usages.ContainsKey(MdlFile.VertexUsage.Color)
|
||||
nColors
|
||||
);
|
||||
|
||||
return materialUsages switch
|
||||
{
|
||||
(2, true) => typeof(VertexTexture2ColorFfxiv),
|
||||
(2, false) => typeof(VertexTexture2),
|
||||
(1, true) => typeof(VertexTexture1ColorFfxiv),
|
||||
(1, false) => typeof(VertexTexture1),
|
||||
(0, true) => typeof(VertexColorFfxiv),
|
||||
(0, false) => typeof(VertexEmpty),
|
||||
(3, 2) => typeof(VertexTexture3Color2Ffxiv),
|
||||
(3, 1) => typeof(VertexTexture3ColorFfxiv),
|
||||
(3, 0) => typeof(VertexTexture3),
|
||||
(2, 2) => typeof(VertexTexture2Color2Ffxiv),
|
||||
(2, 1) => typeof(VertexTexture2ColorFfxiv),
|
||||
(2, 0) => typeof(VertexTexture2),
|
||||
(1, 2) => typeof(VertexTexture1Color2Ffxiv),
|
||||
(1, 1) => typeof(VertexTexture1ColorFfxiv),
|
||||
(1, 0) => typeof(VertexTexture1),
|
||||
(0, 2) => typeof(VertexColor2Ffxiv),
|
||||
(0, 1) => typeof(VertexColorFfxiv),
|
||||
(0, 0) => typeof(VertexEmpty),
|
||||
|
||||
_ => throw new Exception("Unreachable."),
|
||||
_ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary> Build a material vertex from a vertex's attributes. </summary>
|
||||
private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
|
||||
private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary<MdlFile.VertexUsage, List<object>> attributes)
|
||||
{
|
||||
if (_materialType == typeof(VertexEmpty))
|
||||
return new VertexEmpty();
|
||||
|
||||
if (_materialType == typeof(VertexColorFfxiv))
|
||||
return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color]));
|
||||
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
|
||||
|
||||
if (_materialType == typeof(VertexColor2Ffxiv))
|
||||
{
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1));
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture1))
|
||||
return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV]));
|
||||
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
|
||||
|
||||
if (_materialType == typeof(VertexTexture1ColorFfxiv))
|
||||
return new VertexTexture1ColorFfxiv(
|
||||
ToVector2(attributes[MdlFile.VertexUsage.UV]),
|
||||
ToVector4(attributes[MdlFile.VertexUsage.Color])
|
||||
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
|
||||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
||||
);
|
||||
|
||||
if (_materialType == typeof(VertexTexture1Color2Ffxiv))
|
||||
{
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
return new VertexTexture1Color2Ffxiv(
|
||||
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
// XIV packs two UVs into a single vec4 attribute.
|
||||
|
||||
if (_materialType == typeof(VertexTexture2))
|
||||
{
|
||||
var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]);
|
||||
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
|
||||
return new VertexTexture2(
|
||||
new Vector2(uv.X, uv.Y),
|
||||
new Vector2(uv.Z, uv.W)
|
||||
|
|
@ -427,11 +464,63 @@ public class MeshExporter
|
|||
|
||||
if (_materialType == typeof(VertexTexture2ColorFfxiv))
|
||||
{
|
||||
var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]);
|
||||
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
|
||||
return new VertexTexture2ColorFfxiv(
|
||||
new Vector2(uv.X, uv.Y),
|
||||
new Vector2(uv.Z, uv.W),
|
||||
ToVector4(attributes[MdlFile.VertexUsage.Color])
|
||||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture2Color2Ffxiv))
|
||||
{
|
||||
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
|
||||
return new VertexTexture2Color2Ffxiv(
|
||||
new Vector2(uv.X, uv.Y),
|
||||
new Vector2(uv.Z, uv.W),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture3))
|
||||
{
|
||||
// Not 100% sure about this
|
||||
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
|
||||
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
|
||||
return new VertexTexture3(
|
||||
new Vector2(uv0.X, uv0.Y),
|
||||
new Vector2(uv0.Z, uv0.W),
|
||||
new Vector2(uv1.X, uv1.Y)
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture3ColorFfxiv))
|
||||
{
|
||||
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
|
||||
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
|
||||
return new VertexTexture3ColorFfxiv(
|
||||
new Vector2(uv0.X, uv0.Y),
|
||||
new Vector2(uv0.Z, uv0.W),
|
||||
new Vector2(uv1.X, uv1.Y),
|
||||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture3Color2Ffxiv))
|
||||
{
|
||||
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
|
||||
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
|
||||
return new VertexTexture3Color2Ffxiv(
|
||||
new Vector2(uv0.X, uv0.Y),
|
||||
new Vector2(uv0.Z, uv0.W),
|
||||
new Vector2(uv1.X, uv1.Y),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -439,25 +528,20 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
/// <summary> Get the vertex skinning type for this mesh's vertex usages. </summary>
|
||||
private static Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
private Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, List<MdlFile.VertexType>> usages)
|
||||
{
|
||||
if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices))
|
||||
{
|
||||
if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4)
|
||||
{
|
||||
return typeof(VertexJoints8);
|
||||
}
|
||||
else
|
||||
{
|
||||
return typeof(VertexJoints4);
|
||||
}
|
||||
return GetFirstSafe(usages, MdlFile.VertexUsage.BlendWeights) == MdlFile.VertexType.UShort4
|
||||
? typeof(VertexJoints8)
|
||||
: typeof(VertexJoints4);
|
||||
}
|
||||
|
||||
return typeof(VertexEmpty);
|
||||
}
|
||||
|
||||
/// <summary> Build a skinning vertex from a vertex's attributes. </summary>
|
||||
private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
|
||||
private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary<MdlFile.VertexUsage, List<object>> attributes)
|
||||
{
|
||||
if (_skinningType == typeof(VertexEmpty))
|
||||
return new VertexEmpty();
|
||||
|
|
@ -467,8 +551,8 @@ public class MeshExporter
|
|||
if (_boneIndexMap == null)
|
||||
throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available.");
|
||||
|
||||
var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices];
|
||||
var weightsData = attributes[MdlFile.VertexUsage.BlendWeights];
|
||||
var indiciesData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendIndices);
|
||||
var weightsData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendWeights);
|
||||
var indices = ToByteArray(indiciesData);
|
||||
var weights = ToFloatArray(weightsData);
|
||||
|
||||
|
|
@ -495,6 +579,28 @@ public class MeshExporter
|
|||
throw _notifier.Exception($"Unknown skinning type {_skinningType}");
|
||||
}
|
||||
|
||||
/// <summary> Check that the list has length 1 for any case where this is expected and return the one entry. </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private T GetFirstSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
|
||||
{
|
||||
var list = attributes[usage];
|
||||
if (list.Count != 1)
|
||||
throw _notifier.Exception($"Multiple usage indices encountered for {usage}.");
|
||||
|
||||
return list[0];
|
||||
}
|
||||
|
||||
/// <summary> Check that the list has length 2 for any case where this is expected and return both entries. </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private (T First, T Second) GetBothSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
|
||||
{
|
||||
var list = attributes[usage];
|
||||
if (list.Count != 2)
|
||||
throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2.");
|
||||
|
||||
return (list[0], list[1]);
|
||||
}
|
||||
|
||||
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
|
||||
private static Vector2 ToVector2(object data)
|
||||
=> data switch
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using SharpGLTF.Geometry.VertexTypes;
|
||||
using SharpGLTF.Memory;
|
||||
using SharpGLTF.Schema2;
|
||||
|
|
@ -11,7 +10,7 @@ Realistically, it will need to stick around until transforms/mutations are built
|
|||
and there's reason to overhaul the export pipeline.
|
||||
*/
|
||||
|
||||
public struct VertexColorFfxiv : IVertexCustom
|
||||
public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
|
|
@ -20,7 +19,7 @@ public struct VertexColorFfxiv : IVertexCustom
|
|||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector4 FfxivColor;
|
||||
public Vector4 FfxivColor = ffxivColor;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
|
@ -33,9 +32,6 @@ public struct VertexColorFfxiv : IVertexCustom
|
|||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public VertexColorFfxiv(Vector4 ffxivColor)
|
||||
=> FfxivColor = ffxivColor;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{ }
|
||||
|
||||
|
|
@ -88,7 +84,104 @@ public struct VertexColorFfxiv : IVertexCustom
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture1ColorFfxiv : IVertexCustom
|
||||
public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 0;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{ }
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{ }
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
|
|
@ -98,9 +191,9 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
|||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0;
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
|
||||
public Vector4 FfxivColor;
|
||||
public Vector4 FfxivColor = ffxivColor;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
|
@ -113,12 +206,6 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
|||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor)
|
||||
{
|
||||
TexCoord0 = texCoord0;
|
||||
FfxivColor = ffxivColor;
|
||||
}
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
|
|
@ -182,7 +269,119 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture2ColorFfxiv : IVertexCustom
|
||||
public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 1;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex >= 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
|
|
@ -194,9 +393,9 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
|||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0;
|
||||
public Vector2 TexCoord1;
|
||||
public Vector4 FfxivColor;
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector4 FfxivColor = ffxivColor;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
|
@ -209,13 +408,6 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
|||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor)
|
||||
{
|
||||
TexCoord0 = texCoord0;
|
||||
TexCoord1 = texCoord1;
|
||||
FfxivColor = ffxivColor;
|
||||
}
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
|
|
@ -282,3 +474,346 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
|
|||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 2;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
TexCoord1 += delta.TexCoord1Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
1 => TexCoord1,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex >= 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor)
|
||||
: IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_2",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector2 TexCoord2 = texCoord2;
|
||||
public Vector4 FfxivColor = ffxivColor;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 3;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
TexCoord1 += delta.TexCoord1Delta;
|
||||
TexCoord2 += delta.TexCoord2Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
1 => TexCoord1,
|
||||
2 => TexCoord2,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex == 2)
|
||||
TexCoord2 = coord;
|
||||
if (setIndex >= 3)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR":
|
||||
value = FfxivColor;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4)
|
||||
FfxivColor = valueVector4;
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor.X,
|
||||
FfxivColor.Y,
|
||||
FfxivColor.Z,
|
||||
FfxivColor.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0f or > 1f))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1)
|
||||
: IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector2 TexCoord2 = texCoord2;
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 3;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
TexCoord1 += delta.TexCoord1Delta;
|
||||
TexCoord2 += delta.TexCoord2Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
1 => TexCoord1,
|
||||
2 => TexCoord2,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex == 2)
|
||||
TexCoord2 = coord;
|
||||
if (setIndex >= 3)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Files.ModelStructs;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.ModelStructs;
|
||||
using SharpGLTF.Schema2;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
namespace Penumbra.Import.Models.Import;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
namespace Penumbra.Import.Models.Import;
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ public class VertexAttribute
|
|||
|
||||
var normals = normalAccessor.AsVector3Array();
|
||||
var tangents = accessors.TryGetValue("TANGENT", out var accessor)
|
||||
? accessor.AsVector4Array()
|
||||
? accessor.AsVector4Array().ToArray()
|
||||
: CalculateTangents(accessors, indices, normals, notifier);
|
||||
|
||||
if (tangents == null)
|
||||
|
|
|
|||
69
Penumbra/Import/Models/ModelExtensions.cs
Normal file
69
Penumbra/Import/Models/ModelExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
|
@ -63,7 +63,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
|
|||
if (info.FileType is not FileType.Model)
|
||||
return [];
|
||||
|
||||
var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1);
|
||||
var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1);
|
||||
|
||||
return info.ObjectType switch
|
||||
{
|
||||
|
|
@ -79,9 +79,9 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
|
|||
ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear
|
||||
=> [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)],
|
||||
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
|
||||
ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)],
|
||||
ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)],
|
||||
ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)],
|
||||
ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)],
|
||||
ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)],
|
||||
ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)],
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
|
|||
if (targetId == EstEntry.Zero)
|
||||
return [];
|
||||
|
||||
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)];
|
||||
return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)];
|
||||
}
|
||||
|
||||
/// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary>
|
||||
|
|
@ -137,7 +137,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
|
|||
|
||||
var resolvedPath = info.ObjectType switch
|
||||
{
|
||||
ObjectType.Character => GamePaths.Character.Mtrl.Path(
|
||||
ObjectType.Character => GamePaths.Mtrl.Customization(
|
||||
info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant),
|
||||
_ => absolutePath,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System.Xml;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Import.Models.Export;
|
||||
|
||||
namespace Penumbra.Import.Models;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public class SimpleMod
|
|||
public class ModPackPage
|
||||
{
|
||||
public int PageIndex = 0;
|
||||
public ModGroup[] ModGroups = Array.Empty<ModGroup>();
|
||||
public ModGroup[] ModGroups = [];
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
|
@ -34,7 +34,7 @@ public class ModGroup
|
|||
{
|
||||
public string GroupName = string.Empty;
|
||||
public GroupType SelectionType = GroupType.Single;
|
||||
public OptionList[] OptionList = Array.Empty<OptionList>();
|
||||
public OptionList[] OptionList = [];
|
||||
public string Description = string.Empty;
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ public class OptionList
|
|||
public string Name = string.Empty;
|
||||
public string Description = string.Empty;
|
||||
public string ImagePath = string.Empty;
|
||||
public SimpleMod[] ModsJsons = Array.Empty<SimpleMod>();
|
||||
public SimpleMod[] ModsJsons = [];
|
||||
public string GroupName = string.Empty;
|
||||
public GroupType SelectionType = GroupType.Single;
|
||||
public bool IsChecked = false;
|
||||
|
|
@ -59,8 +59,8 @@ public class ExtendedModPack
|
|||
public string Version = string.Empty;
|
||||
public string Description = DefaultTexToolsData.Description;
|
||||
public string Url = string.Empty;
|
||||
public ModPackPage[] ModPackPages = Array.Empty<ModPackPage>();
|
||||
public SimpleMod[] SimpleModsList = Array.Empty<SimpleMod>();
|
||||
public ModPackPage[] ModPackPages = [];
|
||||
public SimpleMod[] SimpleModsList = [];
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
|
@ -72,5 +72,5 @@ public class SimpleModPack
|
|||
public string Version = string.Empty;
|
||||
public string Description = DefaultTexToolsData.Description;
|
||||
public string Url = string.Empty;
|
||||
public SimpleMod[] SimpleModsList = Array.Empty<SimpleMod>();
|
||||
public SimpleMod[] SimpleModsList = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ public partial class TexToolsImporter : IDisposable
|
|||
// Puts out warnings if extension does not correspond to data.
|
||||
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
|
||||
{
|
||||
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar")
|
||||
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".pcp" or ".zip" or ".7z" or ".rar")
|
||||
return HandleRegularArchive(modPackFile);
|
||||
|
||||
using var zfs = modPackFile.OpenRead();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
|||
using OtterGui.Filesystem;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Archives.SevenZip;
|
||||
|
|
@ -96,17 +97,36 @@ public partial class TexToolsImporter
|
|||
|
||||
_token.ThrowIfCancellationRequested();
|
||||
var oldName = _currentModDirectory.FullName;
|
||||
// Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
|
||||
if (leadDir)
|
||||
|
||||
// Try renaming the folder three times because sometimes we get AccessDenied here for some unknown reason.
|
||||
const int numTries = 3;
|
||||
for (var i = 1;; ++i)
|
||||
{
|
||||
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false);
|
||||
Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName);
|
||||
Directory.Delete(oldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false);
|
||||
Directory.Move(oldName, _currentModDirectory.FullName);
|
||||
// Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
|
||||
try
|
||||
{
|
||||
if (leadDir)
|
||||
{
|
||||
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false);
|
||||
Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName);
|
||||
Directory.Delete(oldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false);
|
||||
Directory.Move(oldName, _currentModDirectory.FullName);
|
||||
}
|
||||
}
|
||||
catch (IOException io)
|
||||
{
|
||||
if (i == numTries)
|
||||
throw;
|
||||
|
||||
Penumbra.Log.Warning($"Error when renaming the extracted mod, try {i}/{numTries}: {io.Message}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
_currentModDirectory.Refresh();
|
||||
|
|
@ -127,6 +147,9 @@ public partial class TexToolsImporter
|
|||
case ".mtrl":
|
||||
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
|
||||
break;
|
||||
case ".tex":
|
||||
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
|
||||
break;
|
||||
default:
|
||||
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Import.Structs;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Newtonsoft.Json;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -259,6 +259,7 @@ public partial class TexToolsImporter
|
|||
{
|
||||
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
|
||||
".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data),
|
||||
".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data),
|
||||
_ => data.Data,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Lumina.Extensions;
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Import;
|
||||
|
|
@ -19,9 +18,7 @@ public partial class TexToolsMeta
|
|||
|
||||
var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot);
|
||||
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask;
|
||||
var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask;
|
||||
if (_keepDefault || def != value)
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
|
||||
// Deserialize and check Eqdp Entries and add them to the list if they are non-default.
|
||||
|
|
@ -41,11 +38,9 @@ public partial class TexToolsMeta
|
|||
continue;
|
||||
|
||||
var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr);
|
||||
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
|
||||
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
|
||||
var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask;
|
||||
if (_keepDefault || def != value)
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
|
||||
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,10 +50,9 @@ public partial class TexToolsMeta
|
|||
if (data == null)
|
||||
return;
|
||||
|
||||
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
|
||||
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
|
||||
if (_keepDefault || value != def)
|
||||
MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value);
|
||||
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
|
||||
var identifier = new GmpIdentifier(metaFileInfo.PrimaryId);
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
|
||||
// Deserialize and check Est Entries and add them to the list if they are non-default.
|
||||
|
|
@ -86,9 +80,7 @@ public partial class TexToolsMeta
|
|||
continue;
|
||||
|
||||
var identifier = new EstIdentifier(id, type, gr);
|
||||
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
|
||||
if (_keepDefault || def != value)
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,15 +100,10 @@ public partial class TexToolsMeta
|
|||
{
|
||||
var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId,
|
||||
metaFileInfo.EquipSlot, metaFileInfo.SecondaryType);
|
||||
var file = new ImcFile(_metaFileManager, identifier);
|
||||
var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
|
||||
foreach (var value in values)
|
||||
{
|
||||
identifier = identifier with { Variant = (Variant)i };
|
||||
var def = file.GetEntry(partIdx, (Variant)i);
|
||||
if (_keepDefault || def != value && identifier.Validate())
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Import;
|
||||
|
|
@ -8,7 +7,7 @@ namespace Penumbra.Import;
|
|||
public partial class TexToolsMeta
|
||||
{
|
||||
// Parse a single rgsp file.
|
||||
public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data, bool keepDefault)
|
||||
public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data)
|
||||
{
|
||||
if (data.Length != 45 && data.Length != 42)
|
||||
{
|
||||
|
|
@ -70,9 +69,7 @@ public partial class TexToolsMeta
|
|||
void Add(RspAttribute attribute, float value)
|
||||
{
|
||||
var identifier = new RspIdentifier(subRace, attribute);
|
||||
var def = CmpFile.GetDefault(manager, subRace, attribute);
|
||||
if (keepDefault || value != def.Value)
|
||||
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
|
||||
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,11 @@ public partial class TexToolsMeta
|
|||
// The info class determines the files or table locations the changes need to apply to from the filename.
|
||||
public readonly uint Version;
|
||||
public readonly string FilePath;
|
||||
public readonly MetaDictionary MetaManipulations = new();
|
||||
private readonly bool _keepDefault;
|
||||
public readonly MetaDictionary MetaManipulations = new();
|
||||
|
||||
private readonly MetaFileManager _metaFileManager;
|
||||
|
||||
public TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte[] data, bool keepDefault)
|
||||
public TexToolsMeta(GamePathParser parser, byte[] data)
|
||||
{
|
||||
_metaFileManager = metaFileManager;
|
||||
_keepDefault = keepDefault;
|
||||
try
|
||||
{
|
||||
using var reader = new BinaryReader(new MemoryStream(data));
|
||||
|
|
@ -79,7 +75,6 @@ public partial class TexToolsMeta
|
|||
|
||||
private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version)
|
||||
{
|
||||
_metaFileManager = metaFileManager;
|
||||
FilePath = filePath;
|
||||
Version = version;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ public partial class CombinedTexture : IDisposable
|
|||
{
|
||||
AsIs,
|
||||
Bitmap,
|
||||
BC1,
|
||||
BC3,
|
||||
BC4,
|
||||
BC5,
|
||||
BC7,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,76 @@ public static class TexFileParser
|
|||
return 13;
|
||||
}
|
||||
|
||||
public static unsafe void FixMipOffsets(long size, ref TexFile.TexHeader header, out long newSize)
|
||||
{
|
||||
var width = (uint)header.Width;
|
||||
var height = (uint)header.Height;
|
||||
var format = header.Format.ToDXGI();
|
||||
var bits = format.BitsPerPixel();
|
||||
var totalSize = 80u;
|
||||
size -= totalSize;
|
||||
var minSize = format.IsCompressed() ? 4u : 1u;
|
||||
for (var i = 0; i < 13; ++i)
|
||||
{
|
||||
var requiredSize = (uint)((long)width * height * bits / 8);
|
||||
if (requiredSize > size)
|
||||
{
|
||||
newSize = totalSize;
|
||||
if (header.MipCount != i)
|
||||
{
|
||||
Penumbra.Log.Debug(
|
||||
$"-- Mip Map Count in TEX header was {header.MipCount}, but file only contains data for {i} Mip Maps, fixed.");
|
||||
FixLodOffsets(ref header, i);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (header.OffsetToSurface[i] != totalSize)
|
||||
{
|
||||
Penumbra.Log.Debug(
|
||||
$"-- Mip Map Offset {i + 1} in TEX header was {header.OffsetToSurface[i]} but should be {totalSize}, fixed.");
|
||||
header.OffsetToSurface[i] = totalSize;
|
||||
}
|
||||
|
||||
if (width == minSize && height == minSize)
|
||||
{
|
||||
++i;
|
||||
newSize = totalSize + requiredSize;
|
||||
if (header.MipCount != i)
|
||||
{
|
||||
Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints.");
|
||||
FixLodOffsets(ref header, i);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
totalSize += requiredSize;
|
||||
size -= requiredSize;
|
||||
width = Math.Max(width / 2, minSize);
|
||||
height = Math.Max(height / 2, minSize);
|
||||
}
|
||||
|
||||
newSize = totalSize;
|
||||
if (header.MipCount != 13)
|
||||
{
|
||||
Penumbra.Log.Debug($"-- Mip Map Count in TEX header was {header.MipCount}, but maximum is 13, fixed.");
|
||||
FixLodOffsets(ref header, 13);
|
||||
}
|
||||
|
||||
void FixLodOffsets(ref TexFile.TexHeader header, int index)
|
||||
{
|
||||
header.MipCount = index;
|
||||
if (header.LodOffset[2] >= header.MipCount)
|
||||
header.LodOffset[2] = (byte)(header.MipCount - 1);
|
||||
if (header.LodOffset[1] >= header.MipCount)
|
||||
header.LodOffset[1] = header.MipCount > 2 ? (byte)(header.MipCount - 2) : (byte)(header.MipCount - 1);
|
||||
for (++index; index < 13; ++index)
|
||||
header.OffsetToSurface[index] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void CopyData(ScratchImage image, BinaryReader r)
|
||||
{
|
||||
fixed (byte* ptr = image.Pixels)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data.Files;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
|
|
@ -20,7 +20,7 @@ public static class TextureDrawer
|
|||
{
|
||||
size = texture.TextureWrap.Size.Contain(size);
|
||||
|
||||
ImGui.Image(texture.TextureWrap.ImGuiHandle, size);
|
||||
ImGui.Image(texture.TextureWrap.Handle, size);
|
||||
DrawData(texture);
|
||||
}
|
||||
else if (texture.LoadError != null)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
|
@ -6,15 +7,17 @@ using OtterGui.Log;
|
|||
using OtterGui.Services;
|
||||
using OtterGui.Tasks;
|
||||
using OtterTex;
|
||||
using SharpDX.Direct3D11;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Formats.Tga;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using DxgiDevice = SharpDX.DXGI.Device;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace Penumbra.Import.Textures;
|
||||
|
||||
public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider)
|
||||
public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider, IUiBuilder uiBuilder)
|
||||
: SingleTaskQueue, IDisposable, IService
|
||||
{
|
||||
private readonly Logger _logger = logger;
|
||||
|
|
@ -201,8 +204,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
|
||||
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, width, height),
|
||||
CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, width, height),
|
||||
_ => throw new Exception("Wrong save type."),
|
||||
};
|
||||
|
||||
|
|
@ -320,7 +326,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
}
|
||||
|
||||
/// <summary> Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. </summary>
|
||||
public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null,
|
||||
public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel, byte[]? rgba = null,
|
||||
int width = 0, int height = 0)
|
||||
{
|
||||
switch (input.Type.ReduceToBehaviour())
|
||||
|
|
@ -331,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
cancel.ThrowIfCancellationRequested();
|
||||
var dds = ConvertToDds(rgba, width, height).AsDds!;
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
return CreateCompressed(dds, mipMaps, bc7, cancel);
|
||||
return CreateCompressed(dds, mipMaps, format, cancel);
|
||||
}
|
||||
case TextureType.Dds:
|
||||
{
|
||||
var scratch = input.AsDds!;
|
||||
return CreateCompressed(scratch, mipMaps, bc7, cancel);
|
||||
return CreateCompressed(scratch, mipMaps, format, cancel);
|
||||
}
|
||||
default: return new BaseImage();
|
||||
}
|
||||
|
|
@ -384,9 +390,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
}
|
||||
|
||||
/// <summary> Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. </summary>
|
||||
public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel)
|
||||
public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel)
|
||||
{
|
||||
var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm;
|
||||
if (input.Meta.Format == format)
|
||||
return input;
|
||||
|
||||
|
|
@ -398,6 +403,16 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
|
||||
input = AddMipMaps(input, mipMaps);
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
// See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition.
|
||||
if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)
|
||||
{
|
||||
var device = new Device(uiBuilder.DeviceHandle);
|
||||
var dxgiDevice = device.QueryInterface<DxgiDevice>();
|
||||
|
||||
using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel);
|
||||
return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel);
|
||||
}
|
||||
|
||||
return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel);
|
||||
}
|
||||
|
||||
|
|
|
|||
47
Penumbra/Interop/CloudApi.cs
Normal file
47
Penumbra/Interop/CloudApi.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -59,9 +59,6 @@ public class GameState : IService
|
|||
|
||||
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
|
||||
|
||||
public ResolveData SoundData
|
||||
=> _animationLoadData.Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public ResolveData SetSoundData(ResolveData data)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public unsafe class AtchCallerHook1 : FastHook<AtchCallerHook1.Delegate>, IDispo
|
|||
|
||||
private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel)
|
||||
{
|
||||
var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true);
|
||||
var collection = playerModel.Valid ? _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true) : _collectionResolver.DefaultCollection;
|
||||
_metaState.AtchCollection.Push(collection);
|
||||
Task.Result.Original(data, slot, unk, playerModel);
|
||||
_metaState.AtchCollection.Pop();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
|
|||
{
|
||||
_collectionResolver = collectionResolver;
|
||||
_metaState = metaState;
|
||||
Task = hooks.CreateHook<Delegate>("Change Customize", Sigs.ChangeCustomize, Detour, !HookOverrides.Instance.Meta.ChangeCustomize);
|
||||
Task = hooks.CreateHook<Delegate>("Change Customize", Sigs.UpdateDrawData, Detour, !HookOverrides.Instance.Meta.ChangeCustomize);
|
||||
}
|
||||
|
||||
public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Dalamud.Hooking;
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.UI.AdvancedWindow;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Objects;
|
||||
|
||||
|
|
@ -13,7 +12,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBa
|
|||
/// <seealso cref="PathResolving.DrawObjectState.OnCharacterBaseDestructor"/>
|
||||
DrawObjectState = 0,
|
||||
|
||||
/// <seealso cref="ModEditWindow.MtrlTab.UnbindFromDrawObjectMaterialInstances"/>
|
||||
/// <seealso cref="UI.AdvancedWindow.Materials.MtrlTab.UnbindFromDrawObjectMaterialInstances"/>
|
||||
MtrlTab = -1000,
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBa
|
|||
|
||||
private nint Detour(CharacterBase* characterBase)
|
||||
{
|
||||
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)characterBase:X}.");
|
||||
Penumbra.Log.Excessive($"[{Name}] Triggered with 0x{(nint)characterBase:X}.");
|
||||
Invoke(characterBase);
|
||||
return _task.Result.Original(characterBase);
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue