diff --git a/.editorconfig b/.editorconfig
index c645b573..f0328fd7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -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
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b40b2538..1a61439e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,13 +10,15 @@ jobs:
build:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
with:
submodules: recursive
- name: Setup .NET
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: '8.x.x'
+ dotnet-version: |
+ 10.x.x
+ 9.x.x
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
@@ -29,7 +31,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
- uses: actions/upload-artifact@v2.2.1
+ uses: actions/upload-artifact@v4
with:
path: |
./Penumbra/bin/Release/*
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7c9e2909..c72b4800 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,18 +9,20 @@ jobs:
build:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
with:
submodules: recursive
- name: Setup .NET
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: '8.x.x'
+ dotnet-version: |
+ 10.x.x
+ 9.x.x
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
run: |
- Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
+ Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |
@@ -37,7 +39,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
- uses: actions/upload-artifact@v2.2.1
+ uses: actions/upload-artifact@v4
with:
path: |
./Penumbra/bin/Release/*
diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml
index 91361646..c6b4e459 100644
--- a/.github/workflows/test_release.yml
+++ b/.github/workflows/test_release.yml
@@ -9,18 +9,20 @@ jobs:
build:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
with:
submodules: recursive
- name: Setup .NET
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: '8.x.x'
+ dotnet-version: |
+ 10.x.x
+ 9.x.x
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
run: |
- Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
+ Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |
@@ -37,7 +39,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
- uses: actions/upload-artifact@v2.2.1
+ uses: actions/upload-artifact@v4
with:
path: |
./Penumbra/bin/Debug/*
diff --git a/OtterGui b/OtterGui
index 1d936516..ff1e6543 160000
--- a/OtterGui
+++ b/OtterGui
@@ -1 +1 @@
-Subproject commit 1d9365164655a7cb38172e1311e15e19b1def6db
+Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf
diff --git a/Penumbra.Api b/Penumbra.Api
index 69d106b4..52a3216a 160000
--- a/Penumbra.Api
+++ b/Penumbra.Api
@@ -1 +1 @@
-Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0
+Subproject commit 52a3216a525592205198303df2844435e382cf87
diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
index 3446530a..292be2ff 100644
--- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
+++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
@@ -1,4 +1,6 @@
-using System.Text.Json.Nodes;
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
@@ -55,8 +57,10 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation
accessor.Write(16, characterAddress);
var span = GetSpan(accessor, 24, 16);
collectionId.TryWriteBytes(span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 40);
WriteSpan(characterName, span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
index 4036455d..89fea29d 100644
--- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
+++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
@@ -1,4 +1,6 @@
-using System.Text.Json.Nodes;
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
@@ -38,8 +40,10 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 16);
collectionId.TryWriteBytes(span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 36);
WriteSpan(characterName, span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
diff --git a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
index a1b3de52..e2ffcebe 100644
--- a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
+++ b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
@@ -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;
diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
index 03f63ba4..e4ee66d0 100644
--- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
+++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
@@ -1,4 +1,6 @@
-using System.Text.Json.Nodes;
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
@@ -44,12 +46,16 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 16);
collectionId.TryWriteBytes(span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 36, 80);
WriteSpan(characterName, span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 116, 260);
WriteSpan(requestedFileName, span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
span = GetSpan(accessor, 376);
WriteSpan(actualFileName, span);
+ accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs
index cdac103f..55460548 100644
--- a/Penumbra.CrashHandler/CrashData.cs
+++ b/Penumbra.CrashHandler/CrashData.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;
@@ -55,7 +57,7 @@ public class CrashData
/// The last vfx function invoked before this crash data was generated.
public VfxFuncInvokedEntry? LastVfxFuncInvoked
- => LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0];
+ => LastVFXFuncsInvoked.Count == 0 ? default : LastVFXFuncsInvoked[0];
/// A collection of the last few characters loaded before this crash data was generated.
public List LastCharactersLoaded { get; set; } = [];
@@ -64,5 +66,5 @@ public class CrashData
public List LastModdedFilesLoaded { get; set; } = [];
/// A collection of the last few vfx functions invoked before this crash data was generated.
- public List LastVfxFuncsInvoked { get; set; } = [];
+ public List LastVFXFuncsInvoked { get; set; } = [];
}
diff --git a/Penumbra.CrashHandler/GameEventLogReader.cs b/Penumbra.CrashHandler/GameEventLogReader.cs
index 1813a671..8a7f53f8 100644
--- a/Penumbra.CrashHandler/GameEventLogReader.cs
+++ b/Penumbra.CrashHandler/GameEventLogReader.cs
@@ -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;
diff --git a/Penumbra.CrashHandler/GameEventLogWriter.cs b/Penumbra.CrashHandler/GameEventLogWriter.cs
index e2c461f4..915c59a2 100644
--- a/Penumbra.CrashHandler/GameEventLogWriter.cs
+++ b/Penumbra.CrashHandler/GameEventLogWriter.cs
@@ -1,4 +1,5 @@
-using Penumbra.CrashHandler.Buffers;
+using System;
+using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;
diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
index c9f97fde..e07bb745 100644
--- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
+++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
@@ -1,20 +1,6 @@
-
-
+
Exe
- net8.0-windows
- preview
- enable
- x64
- enable
- true
- false
-
-
-
- $(appdata)\XIVLauncher\addon\Hooks\dev\
- $(HOME)/.xlcore/dalamud/Hooks/dev/
- $(DALAMUD_HOME)/
@@ -25,4 +11,8 @@
embedded
+
+ false
+
+
diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs
index 3bc461f7..38c176a6 100644
--- a/Penumbra.CrashHandler/Program.cs
+++ b/Penumbra.CrashHandler/Program.cs
@@ -1,4 +1,6 @@
-using System.Diagnostics;
+using System;
+using System.Diagnostics;
+using System.IO;
using System.Text.Json;
namespace Penumbra.CrashHandler;
diff --git a/Penumbra.CrashHandler/packages.lock.json b/Penumbra.CrashHandler/packages.lock.json
new file mode 100644
index 00000000..0a160ea5
--- /dev/null
+++ b/Penumbra.CrashHandler/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net10.0-windows7.0": {
+ "DotNet.ReproducibleBuilds": {
+ "type": "Direct",
+ "requested": "[1.2.39, )",
+ "resolved": "1.2.39",
+ "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Penumbra.GameData b/Penumbra.GameData
index f2cea65b..0e973ed6 160000
--- a/Penumbra.GameData
+++ b/Penumbra.GameData
@@ -1 +1 @@
-Subproject commit f2cea65b83b2d6cb0d03339e8f76aed8102a41d5
+Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60
diff --git a/Penumbra.String b/Penumbra.String
index caa58c5c..9bd016fb 160000
--- a/Penumbra.String
+++ b/Penumbra.String
@@ -1 +1 @@
-Subproject commit caa58c5c92710e69ce07b9d736ebe2d228cb4488
+Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592
diff --git a/Penumbra.sln b/Penumbra.sln
index 78fa1543..fbcd6080 100644
--- a/Penumbra.sln
+++ b/Penumbra.sln
@@ -8,6 +8,11 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ .github\workflows\build.yml = .github\workflows\build.yml
+ Penumbra\Penumbra.json = Penumbra\Penumbra.json
+ .github\workflows\release.yml = .github\workflows\release.yml
+ repo.json = repo.json
+ .github\workflows\test_release.yml = .github\workflows\test_release.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}"
@@ -18,42 +23,76 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Ap
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}"
+ ProjectSection(SolutionItems) = preProject
+ schemas\default_mod.json = schemas\default_mod.json
+ schemas\group.json = schemas\group.json
+ schemas\local_mod_data-v3.json = schemas\local_mod_data-v3.json
+ schemas\mod_meta-v3.json = schemas\mod_meta-v3.json
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}"
+ ProjectSection(SolutionItems) = preProject
+ schemas\structs\container.json = schemas\structs\container.json
+ schemas\structs\group_combining.json = schemas\structs\group_combining.json
+ schemas\structs\group_imc.json = schemas\structs\group_imc.json
+ schemas\structs\group_multi.json = schemas\structs\group_multi.json
+ schemas\structs\group_single.json = schemas\structs\group_single.json
+ schemas\structs\manipulation.json = schemas\structs\manipulation.json
+ schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
+ schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
+ schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
+ schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
+ schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
+ schemas\structs\meta_est.json = schemas\structs\meta_est.json
+ schemas\structs\meta_geqp.json = schemas\structs\meta_geqp.json
+ schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
+ schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
+ schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
+ schemas\structs\meta_shp.json = schemas\structs\meta_shp.json
+ schemas\structs\option.json = schemas\structs\option.json
+ EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
+ Debug|x64 = Debug|x64
+ Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU
- {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU
- {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU
- {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU
- {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU
- {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
+ {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64
+ {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64
+ {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64
+ {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64
+ {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64
+ {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64
+ {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64
+ {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64
+ {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64
+ {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64
+ {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64
+ {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64
+ {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64
+ {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64
+ {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64
+ {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64
+ {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64
+ {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64
+ {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64
+ {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {BFEA7504-1210-4F79-A7FE-BF03B6567E33} = {F89C9EAE-25C8-43BE-8108-5921E5A93502}
+ {B03F276A-0572-4F62-AF86-EF62F6B80463} = {BFEA7504-1210-4F79-A7FE-BF03B6567E33}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
EndGlobalSection
diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs
index ff393aaf..c40feb12 100644
--- a/Penumbra/Api/Api/CollectionApi.cs
+++ b/Penumbra/Api/Api/CollectionApi.cs
@@ -2,13 +2,14 @@ using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
+using Penumbra.Mods;
namespace Penumbra.Api.Api;
public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService
{
public Dictionary GetCollections()
- => collections.Storage.ToDictionary(c => c.Id, c => c.Name);
+ => collections.Storage.ToDictionary(c => c.Identity.Id, c => c.Identity.Name);
public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier)
{
@@ -17,17 +18,33 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
var list = new List<(Guid Id, string Name)>(4);
if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty)
- list.Add((collection.Id, collection.Name));
+ list.Add((collection.Identity.Id, collection.Identity.Name));
else if (identifier.Length >= 8)
- list.AddRange(collections.Storage.Where(c => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase))
- .Select(c => (c.Id, c.Name)));
+ list.AddRange(collections.Storage.Where(c => c.Identity.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase))
+ .Select(c => (c.Identity.Id, c.Identity.Name)));
list.AddRange(collections.Storage
- .Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name)))
- .Select(c => (c.Id, c.Name)));
+ .Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase)
+ && !list.Contains((c.Identity.Id, c.Identity.Name)))
+ .Select(c => (c.Identity.Id, c.Identity.Name)));
return list;
}
+ public Func CheckCurrentChangedItemFunc()
+ {
+ var weakRef = new WeakReference(collections);
+ return s =>
+ {
+ if (!weakRef.TryGetTarget(out var c))
+ throw new ObjectDisposedException("The underlying collection storage of this IPC container was disposed.");
+
+ if (!c.Active.Current.ChangedItems.TryGetValue(s, out var d))
+ return [];
+
+ return d.Item1.Select(m => (m is Mod mod ? mod.Identifier : string.Empty, m.Name.Text)).ToArray();
+ };
+ }
+
public Dictionary GetChangedItemsForCollection(Guid collectionId)
{
try
@@ -36,7 +53,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
collection = ModCollection.Empty;
if (collection.HasCache)
- return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2);
+ return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject());
Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
return [];
@@ -54,7 +71,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
return null;
var collection = collections.Active.ByType((CollectionType)type);
- return collection == null ? null : (collection.Id, collection.Name);
+ return collection == null ? null : (collection.Identity.Id, collection.Identity.Name);
}
internal (Guid Id, string Name)? GetCollection(byte type)
@@ -64,17 +81,18 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
- return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name));
+ return (false, false, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
if (collections.Active.Individuals.TryGetValue(id, out var collection))
- return (true, true, (collection.Id, collection.Name));
+ return (true, true, (collection.Identity.Id, collection.Identity.Name));
helpers.AssociatedCollection(gameObjectIdx, out collection);
- return (true, false, (collection.Id, collection.Name));
+ return (true, false, (collection.Identity.Id, collection.Identity.Name));
}
public Guid[] GetCollectionByName(string name)
- => collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray();
+ => collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id)
+ .ToArray();
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId,
bool allowCreateNew, bool allowDelete)
@@ -83,7 +101,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
return (PenumbraApiEc.InvalidArgument, null);
var oldCollection = collections.Active.ByType((CollectionType)type);
- var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?();
+ var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple?();
if (collectionId == null)
{
if (old == null)
@@ -106,7 +124,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
collections.Active.CreateSpecialCollection((CollectionType)type);
}
- else if (old.Value.Item1 == collection.Id)
+ else if (old.Value.Item1 == collection.Identity.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
@@ -120,10 +138,10 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
- return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name));
+ return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null;
- var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?();
+ var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple?();
if (collectionId == null)
{
if (old == null)
@@ -148,7 +166,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
var ids = collections.Active.Individuals.GetGroup(id);
collections.Active.CreateIndividualCollection(ids);
}
- else if (old.Value.Item1 == collection.Id)
+ else if (old.Value.Item1 == collection.Identity.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs
index 93345053..5a1fc347 100644
--- a/Penumbra/Api/Api/EditingApi.cs
+++ b/Penumbra/Api/Api/EditingApi.cs
@@ -10,6 +10,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
=> textureType switch
{
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
+ TextureType.Targa => textureManager.SaveTga(inputFile, outputFile),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile),
@@ -18,6 +19,12 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile),
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile),
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile),
+ TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, inputFile, outputFile),
+ TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, inputFile, outputFile),
+ TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, inputFile, outputFile),
+ TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, inputFile, outputFile),
+ TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, inputFile, outputFile),
+ TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, inputFile, outputFile),
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
};
@@ -26,6 +33,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
=> textureType switch
{
TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Targa => textureManager.SaveTga(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
@@ -34,6 +42,12 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
_ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
};
// @formatter:on
diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs
index becb55ee..74cde3a0 100644
--- a/Penumbra/Api/Api/GameStateApi.cs
+++ b/Penumbra/Api/Api/GameStateApi.cs
@@ -2,8 +2,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
+using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.PathResolving;
-using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs;
using Penumbra.Services;
using Penumbra.String.Classes;
@@ -14,23 +14,27 @@ 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);
}
public unsafe void Dispose()
{
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
+ _resourceLoader.PapRequested -= OnPapRequested;
_communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
}
@@ -59,24 +63,61 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject)
{
var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
- return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name));
+ return (data.AssociatedGameObject, (Id: data.ModCollection.Identity.Id, Name: data.ModCollection.Identity.Name));
}
public int GetCutsceneParentIndex(int actorIdx)
=> _cutsceneService.GetParentIndex(actorIdx);
+ public Func GetCutsceneParentIndexFunc()
+ {
+ var weakRef = new WeakReference(_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 GetGameObjectFromDrawObjectFunc()
+ {
+ var weakRef = new WeakReference(_drawObjectState);
+ return model =>
+ {
+ if (!weakRef.TryGetTarget(out var c))
+ throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed.");
+
+ return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero;
+ };
+ }
+
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
- ? PenumbraApiEc.Success
+ ? PenumbraApiEc.Success
: PenumbraApiEc.InvalidArgument;
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
{
- if (resolveData.AssociatedGameObject != nint.Zero)
- GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(),
- manipulatedPath?.ToString() ?? originalPath.ToString());
+ if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
+ {
+ var original = originalPath.ToString();
+ GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
+ manipulatedPath?.ToString() ?? original);
+ }
+ }
+
+ private void OnPapRequested(Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
+ {
+ if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null)
+ {
+ var original = originalPath.ToString();
+ GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original,
+ manipulatedPath?.ToString() ?? original);
+ }
}
private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject)
- => CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject);
+ => CreatedCharacterBase?.Invoke(gameObject, collection.Identity.Id, drawObject);
}
diff --git a/Penumbra/Api/Api/IdentityChecker.cs b/Penumbra/Api/Api/IdentityChecker.cs
new file mode 100644
index 00000000..e090053e
--- /dev/null
+++ b/Penumbra/Api/Api/IdentityChecker.cs
@@ -0,0 +1,7 @@
+namespace Penumbra.Api.Api;
+
+public static class IdentityChecker
+{
+ public static bool Check(string identity)
+ => true;
+}
diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs
index c467df58..5cffc811 100644
--- a/Penumbra/Api/Api/MetaApi.cs
+++ b/Penumbra/Api/Api/MetaApi.cs
@@ -1,23 +1,544 @@
+using Dalamud.Plugin.Services;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Services;
+using Penumbra.Collections;
+using Penumbra.Collections.Cache;
+using Penumbra.GameData.Files.AtchStructs;
+using Penumbra.GameData.Files.Utility;
+using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Api.Api;
-public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
+public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers)
+ : IPenumbraApiMeta, IApiService
{
public string GetPlayerMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
- var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
- return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
+ return CompressMetaManipulations(collection);
}
public string GetMetaManipulations(int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
- var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
- return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
+ return CompressMetaManipulations(collection);
+ }
+
+ public Task GetPlayerMetaManipulationsAsync()
+ {
+ return Task.Run(async () =>
+ {
+ var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
+ return CompressMetaManipulations(playerCollection);
+ });
+ }
+
+ public Task GetMetaManipulationsAsync(int gameObjectIdx)
+ {
+ return Task.Run(async () =>
+ {
+ var playerCollection = await framework.RunOnFrameworkThread(() =>
+ {
+ helpers.AssociatedCollection(gameObjectIdx, out var collection);
+ return collection;
+ }).ConfigureAwait(false);
+ return CompressMetaManipulations(playerCollection);
+ });
+ }
+
+ internal static string CompressMetaManipulations(ModCollection collection)
+ => CompressMetaManipulationsV1(collection);
+
+ private static string CompressMetaManipulationsV0(ModCollection collection)
+ {
+ var array = new JArray();
+ if (collection.MetaCache is { } cache)
+ {
+ MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key));
+ MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry)));
+ }
+
+ return Functions.ToCompressedBase64(array, 0);
+ }
+
+ private static unsafe string CompressMetaManipulationsV1(ModCollection? collection)
+ {
+ using var ms = new MemoryStream();
+ ms.Capacity = 1024;
+ using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
+ {
+ zipStream.Write((byte)1);
+ zipStream.Write("META0001"u8);
+ if (collection?.MetaCache is not { } cache)
+ {
+ zipStream.Write(0);
+ zipStream.Write(0);
+ zipStream.Write(0);
+ zipStream.Write(0);
+ zipStream.Write(0);
+ zipStream.Write(0);
+ zipStream.Write(0);
+ }
+ else
+ {
+ WriteCache(zipStream, cache.Imc);
+ WriteCache(zipStream, cache.Eqp);
+ WriteCache(zipStream, cache.Eqdp);
+ WriteCache(zipStream, cache.Est);
+ WriteCache(zipStream, cache.Rsp);
+ WriteCache(zipStream, cache.Gmp);
+ cache.GlobalEqp.EnterReadLock();
+
+ try
+ {
+ zipStream.Write(cache.GlobalEqp.Count);
+ foreach (var (globalEqp, _) in cache.GlobalEqp)
+ zipStream.Write(new ReadOnlySpan(&globalEqp, sizeof(GlobalEqpManipulation)));
+ }
+ finally
+ {
+ cache.GlobalEqp.ExitReadLock();
+ }
+
+ WriteCache(zipStream, cache.Atch);
+ WriteCache(zipStream, cache.Shp);
+ WriteCache(zipStream, cache.Atr);
+ }
+ }
+
+ ms.Flush();
+ ms.Position = 0;
+ var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
+ return Convert.ToBase64String(data);
+
+ void WriteCache(Stream stream, MetaCacheBase metaCache)
+ where TKey : unmanaged, IMetaIdentifier
+ where TValue : unmanaged
+ {
+ metaCache.EnterReadLock();
+ try
+ {
+ stream.Write(metaCache.Count);
+ foreach (var (identifier, (_, value)) in metaCache)
+ {
+ stream.Write(identifier);
+ stream.Write(value);
+ }
+ }
+ finally
+ {
+ metaCache.ExitReadLock();
+ }
+ }
+ }
+
+ public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8);
+ public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8);
+ public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P';
+ public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8);
+ public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8);
+ public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8);
+ public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P';
+ public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H';
+ public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8);
+ public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8);
+
+ private static unsafe string CompressMetaManipulationsV2(ModCollection? collection)
+ {
+ using var ms = new MemoryStream();
+ ms.Capacity = 1024;
+ using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
+ {
+ zipStream.Write((byte)2);
+ zipStream.Write("META0002"u8);
+ if (collection?.MetaCache is { } cache)
+ {
+ WriteCache(zipStream, cache.Imc, ImcKey);
+ WriteCache(zipStream, cache.Eqp, EqpKey);
+ WriteCache(zipStream, cache.Eqdp, EqdpKey);
+ WriteCache(zipStream, cache.Est, EstKey);
+ WriteCache(zipStream, cache.Rsp, RspKey);
+ WriteCache(zipStream, cache.Gmp, GmpKey);
+ cache.GlobalEqp.EnterReadLock();
+
+ try
+ {
+ if (cache.GlobalEqp.Count > 0)
+ {
+ zipStream.Write(GeqpKey);
+ zipStream.Write(cache.GlobalEqp.Count);
+ foreach (var (globalEqp, _) in cache.GlobalEqp)
+ zipStream.Write(new ReadOnlySpan(&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(Stream stream, MetaCacheBase 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();
+ }
+ }
+ }
+
+ ///
+ /// Convert manipulations from a transmitted base64 string to actual manipulations.
+ /// The empty string is treated as an empty set.
+ /// Only returns true if all conversions are successful and distinct.
+ ///
+ internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips, out byte version)
+ {
+ if (manipString.Length == 0)
+ {
+ manips = new MetaDictionary();
+ version = byte.MaxValue;
+ return true;
+ }
+
+ try
+ {
+ var bytes = Convert.FromBase64String(manipString);
+ using var compressedStream = new MemoryStream(bytes);
+ using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
+ using var resultStream = new MemoryStream();
+ zipStream.CopyTo(resultStream);
+ resultStream.Flush();
+ resultStream.Position = 0;
+ var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length);
+ version = data[0];
+ data = data[1..];
+ switch (version)
+ {
+ case 0: return ConvertManipsV0(data, out manips);
+ case 1: return ConvertManipsV1(data, out manips);
+ case 2: return ConvertManipsV2(data, out manips);
+ default:
+ Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
+ manips = null;
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}");
+ manips = null;
+ version = byte.MaxValue;
+ return false;
+ }
+ }
+
+ private static bool ConvertManipsV2(ReadOnlySpan 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();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case EqpKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case EqdpKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case EstKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case RspKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case GmpKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case GeqpKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier))
+ return false;
+ }
+
+ break;
+ case AtchKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case ShpKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ case AtrKey:
+ for (var i = 0; i < count; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool ConvertManipsV1(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips)
+ {
+ if (!data.StartsWith("META0001"u8))
+ {
+ Penumbra.Log.Debug($"Invalid manipulations of version 1, does not start with valid prefix.");
+ manips = null;
+ return false;
+ }
+
+ manips = new MetaDictionary();
+ var r = new SpanBinaryReader(data[8..]);
+ var imcCount = r.ReadInt32();
+ for (var i = 0; i < imcCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ var eqpCount = r.ReadInt32();
+ for (var i = 0; i < eqpCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ var eqdpCount = r.ReadInt32();
+ for (var i = 0; i < eqdpCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ var estCount = r.ReadInt32();
+ for (var i = 0; i < estCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ var rspCount = r.ReadInt32();
+ for (var i = 0; i < rspCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ var gmpCount = r.ReadInt32();
+ for (var i = 0; i < gmpCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ var globalEqpCount = r.ReadInt32();
+ for (var i = 0; i < globalEqpCount; ++i)
+ {
+ var manip = r.Read();
+ if (!manip.Validate() || !manips.TryAdd(manip))
+ return false;
+ }
+
+ // Atch was added after there were already some V1 around, so check for size here.
+ if (r.Position < r.Count)
+ {
+ var atchCount = r.ReadInt32();
+ for (var i = 0; i < atchCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ 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();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+
+ var atrCount = r.ReadInt32();
+ for (var i = 0; i < atrCount; ++i)
+ {
+ var identifier = r.Read();
+ var value = r.Read();
+ if (!identifier.Validate() || !manips.TryAdd(identifier, value))
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static bool ConvertManipsV0(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips)
+ {
+ var json = Encoding.UTF8.GetString(data);
+ manips = JsonConvert.DeserializeObject(json);
+ return manips != null;
+ }
+
+ internal void TestMetaManipulations()
+ {
+ var collection = collectionResolver.PlayerCollection();
+ var dict = new MetaDictionary(collection.MetaCache);
+ var count = dict.Count;
+
+ var watch = Stopwatch.StartNew();
+ var v0 = CompressMetaManipulationsV0(collection);
+ var v0Time = watch.ElapsedMilliseconds;
+
+ watch.Restart();
+ var v1 = CompressMetaManipulationsV1(collection);
+ var v1Time = watch.ElapsedMilliseconds;
+
+ watch.Restart();
+ var v1Success = ConvertManips(v1, out var v1Roundtrip, out _);
+ var v1RoundtripTime = watch.ElapsedMilliseconds;
+
+ watch.Restart();
+ var v0Success = ConvertManips(v0, out var v0Roundtrip, out _);
+ var v0RoundtripTime = watch.ElapsedMilliseconds;
+
+ Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal");
+ Penumbra.Log.Information(
+ $"0 | {count} | {v0Time} | {v0.Length} | {v0Success} | {v0Roundtrip?.Count} | {v0RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
+ Penumbra.Log.Information(
+ $"1 | {count} | {v1Time} | {v1.Length} | {v1Success} | {v1Roundtrip?.Count} | {v1RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
}
}
diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs
index 56b80e63..d49c2904 100644
--- a/Penumbra/Api/Api/ModSettingsApi.cs
+++ b/Penumbra/Api/Api/ModSettingsApi.cs
@@ -1,4 +1,4 @@
-using OtterGui;
+using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
@@ -63,13 +63,39 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
return new AvailableModSettings(dict);
}
- public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName)
- => _modManager.TryGetMod(modDirectory, modName, out var mod)
- ? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type))
- : null;
-
public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory,
string modName, bool ignoreInheritance)
+ {
+ var ret = GetCurrentModSettingsWithTemp(collectionId, modDirectory, modName, ignoreInheritance, true, 0);
+ if (ret.Item2 is null)
+ return (ret.Item1, null);
+
+ return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4));
+ }
+
+ public PenumbraApiEc GetSettingsInAllCollections(string modDirectory, string modName,
+ out Dictionary>, bool, bool)> settings,
+ bool ignoreTemporaryCollections = false)
+ {
+ settings = [];
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return PenumbraApiEc.ModMissing;
+
+ var collections = ignoreTemporaryCollections
+ ? _collectionManager.Storage.Where(c => c != ModCollection.Empty)
+ : _collectionManager.Storage.Where(c => c != ModCollection.Empty).Concat(_collectionManager.Temp.Values);
+ settings = [];
+ foreach (var collection in collections)
+ {
+ if (GetCurrentSettings(collection, mod, false, false, 0) is { } s)
+ settings.Add(collection.Identity.Id, s);
+ }
+
+ return PenumbraApiEc.Success;
+ }
+
+ public (PenumbraApiEc, (bool, int, Dictionary>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId,
+ string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return (PenumbraApiEc.ModMissing, null);
@@ -77,17 +103,32 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return (PenumbraApiEc.CollectionMissing, null);
- var settings = collection.Id == Guid.Empty
- ? null
- : ignoreInheritance
- ? collection.Settings[mod.Index]
- : collection[mod.Index].Settings;
- if (settings == null)
+ if (collection.Identity.Id == Guid.Empty)
return (PenumbraApiEc.Success, null);
- var (enabled, priority, dict) = settings.ConvertToShareable(mod);
- return (PenumbraApiEc.Success,
- (enabled, priority.Value, dict, collection.Settings[mod.Index] == null));
+ if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings)
+ return (PenumbraApiEc.Success, settings);
+
+ return (PenumbraApiEc.Success, null);
+ }
+
+ public (PenumbraApiEc, Dictionary>, bool, bool)>?) GetAllModSettings(Guid collectionId,
+ bool ignoreInheritance, bool ignoreTemporary, int key)
+ {
+ if (!_collectionManager.Storage.ById(collectionId, out var collection))
+ return (PenumbraApiEc.CollectionMissing, null);
+
+ if (collection.Identity.Id == Guid.Empty)
+ return (PenumbraApiEc.Success, []);
+
+ var ret = new Dictionary>, bool, bool)>(_modManager.Count);
+ foreach (var mod in _modManager)
+ {
+ if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings)
+ ret[mod.Identifier] = settings;
+ }
+
+ return (PenumbraApiEc.Success, ret);
}
public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit)
@@ -160,11 +201,11 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
- var setting = mod.Groups[groupIdx] switch
+ var setting = mod.Groups[groupIdx].Behaviour switch
{
- MultiModGroup => Setting.Multi(optionIdx),
- SingleModGroup => Setting.Single(optionIdx),
- _ => Setting.Zero,
+ GroupDrawBehaviour.MultiSelection => Setting.Multi(optionIdx),
+ GroupDrawBehaviour.SingleSelection => Setting.Single(optionIdx),
+ _ => Setting.Zero,
};
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
? PenumbraApiEc.Success
@@ -184,36 +225,9 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
- var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
- if (groupIdx < 0)
- return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
-
- var setting = Setting.Zero;
- switch (mod.Groups[groupIdx])
- {
- case SingleModGroup single:
- {
- var optionIdx = optionNames.Count == 0 ? -1 : single.OptionData.IndexOf(o => o.Name == optionNames[^1]);
- if (optionIdx < 0)
- return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
-
- setting = Setting.Single(optionIdx);
- break;
- }
- case MultiModGroup multi:
- {
- foreach (var name in optionNames)
- {
- var optionIdx = multi.OptionData.IndexOf(o => o.Mod.Name == name);
- if (optionIdx < 0)
- return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
-
- setting |= Setting.Multi(optionIdx);
- }
-
- break;
- }
- }
+ var settingSuccess = ConvertModSetting(mod, optionGroupName, optionNames, out var groupIdx, out var setting);
+ if (settingSuccess is not PenumbraApiEc.Success)
+ return ApiHelpers.Return(settingSuccess, args);
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
? PenumbraApiEc.Success
@@ -238,13 +252,38 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private (bool, int, Dictionary>, bool, bool)? GetCurrentSettings(ModCollection collection, Mod mod,
+ bool ignoreInheritance, bool ignoreTemporary, int key)
+ {
+ var settings = collection.Settings.Settings[mod.Index];
+ if (!ignoreTemporary && settings.TempSettings is { } tempSettings && (tempSettings.Lock <= 0 || tempSettings.Lock == key))
+ {
+ if (!tempSettings.ForceInherit)
+ return (tempSettings.Enabled, tempSettings.Priority.Value, tempSettings.ConvertToShareable(mod).Settings,
+ false, true);
+ if (!ignoreInheritance && collection.GetActualSettings(mod.Index).Settings is { } actualSettingsTemp)
+ return (actualSettingsTemp.Enabled, actualSettingsTemp.Priority.Value,
+ actualSettingsTemp.ConvertToShareable(mod).Settings, true, true);
+ }
+
+ if (settings.Settings is { } ownSettings)
+ return (ownSettings.Enabled, ownSettings.Priority.Value, ownSettings.ConvertToShareable(mod).Settings, false,
+ false);
+ if (!ignoreInheritance && collection.GetInheritedSettings(mod.Index).Settings is { } actualSettings)
+ return (actualSettings.Enabled, actualSettings.Priority.Value,
+ actualSettings.ConvertToShareable(mod).Settings, true, false);
+
+ return null;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void TriggerSettingEdited(Mod mod)
{
var collection = _collectionResolver.PlayerCollection();
- var (settings, parent) = collection[mod.Index];
+ var (settings, parent) = collection.GetActualSettings(mod.Index);
if (settings is { Enabled: true })
- ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection);
+ ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection);
}
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
@@ -254,9 +293,10 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited)
- => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited);
+ => ModSettingChanged?.Invoke(type, collection.Identity.Id, mod?.ModPath.Name ?? string.Empty, inherited);
- private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex)
+ private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
+ int moveIndex)
{
switch (type)
{
@@ -282,4 +322,41 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
TriggerSettingEdited(mod);
}
+
+ public static PenumbraApiEc ConvertModSetting(Mod mod, string groupName, IReadOnlyList optionNames, out int groupIndex,
+ out Setting setting)
+ {
+ groupIndex = mod.Groups.IndexOf(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase));
+ setting = Setting.Zero;
+ if (groupIndex < 0)
+ return PenumbraApiEc.OptionGroupMissing;
+
+ switch (mod.Groups[groupIndex])
+ {
+ case { Behaviour: GroupDrawBehaviour.SingleSelection } single:
+ {
+ var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]);
+ if (optionIdx < 0)
+ return PenumbraApiEc.OptionMissing;
+
+ setting = Setting.Single(optionIdx);
+ break;
+ }
+ case { Behaviour: GroupDrawBehaviour.MultiSelection } multi:
+ {
+ foreach (var name in optionNames)
+ {
+ var optionIdx = multi.Options.IndexOf(o => o.Name == name);
+ if (optionIdx < 0)
+ return PenumbraApiEc.OptionMissing;
+
+ setting |= Setting.Multi(optionIdx);
+ }
+
+ break;
+ }
+ }
+
+ return PenumbraApiEc.Success;
+ }
}
diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs
index c1e0c684..1f4f1cf4 100644
--- a/Penumbra/Api/Api/ModsApi.cs
+++ b/Penumbra/Api/Api/ModsApi.cs
@@ -1,3 +1,4 @@
+using Newtonsoft.Json.Linq;
using OtterGui.Compression;
using OtterGui.Services;
using Penumbra.Api.Enums;
@@ -15,15 +16,17 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
private readonly ModImportManager _modImportManager;
private readonly Configuration _config;
private readonly ModFileSystem _modFileSystem;
+ private readonly MigrationManager _migrationManager;
public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem,
- CommunicatorService communicator)
+ CommunicatorService communicator, MigrationManager migrationManager)
{
_modManager = modManager;
_modImportManager = modImportManager;
_config = config;
_modFileSystem = modFileSystem;
_communicator = communicator;
+ _migrationManager = migrationManager;
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods);
}
@@ -31,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;
@@ -44,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
}
public void Dispose()
- => _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
+ {
+ _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
+ }
public Dictionary GetModList()
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
@@ -75,13 +76,22 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
if (!dir.Exists)
return ApiHelpers.Return(PenumbraApiEc.FileMissing, args);
- if (_modManager.BasePath.FullName != dir.Parent?.FullName)
+ if (dir.Parent == null
+ || Path.TrimEndingDirectorySeparator(Path.GetFullPath(_modManager.BasePath.FullName))
+ != Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
- _modManager.AddMod(dir);
+ _modManager.AddMod(dir, true);
+ if (_config.MigrateImportedModelsToV6)
+ {
+ _migrationManager.MigrateMdlDirectory(dir.FullName, false);
+ _migrationManager.Await();
+ }
+
if (_config.UseFileSystemCompression)
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
- CompressionAlgorithm.Xpress8K);
+ CompressionAlgorithm.Xpress8K, false);
+
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
@@ -98,16 +108,28 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
public event Action? ModAdded;
public event Action? ModMoved;
+ public event Action? CreatingPcp
+ {
+ add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi);
+ remove => _communicator.PcpCreation.Unsubscribe(value!);
+ }
+
+ public event Action? 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();
var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath);
- var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name);
- return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault );
+ var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name);
+ return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault);
}
public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath)
@@ -116,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
@@ -129,4 +151,15 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
return PenumbraApiEc.PathRenameFailed;
}
}
+
+ public Dictionary GetChangedItems(string modDirectory, string modName)
+ => _modManager.TryGetMod(modDirectory, modName, out var mod)
+ ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject())
+ : [];
+
+ public IReadOnlyDictionary> GetChangedItemAdapterDictionary()
+ => new ModChangedItemAdapter(new WeakReference(_modManager));
+
+ public IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)> GetChangedItemAdapterList()
+ => new ModChangedItemAdapter(new WeakReference(_modManager));
}
diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs
index 1d5b1537..c4026c72 100644
--- a/Penumbra/Api/Api/PenumbraApi.cs
+++ b/Penumbra/Api/Api/PenumbraApi.cs
@@ -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, 0);
+ => (BreakingVersion, FeatureVersion);
public bool Valid { get; private set; } = true;
public IPenumbraApiCollection Collection { get; } = collection;
diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs
index d69df448..f74553d1 100644
--- a/Penumbra/Api/Api/PluginStateApi.cs
+++ b/Penumbra/Api/Api/PluginStateApi.cs
@@ -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? 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? 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 SupportedFeatures
+ => FeatureChecker.SupportedFeatures.ToFrozenSet();
+
+ public string[] CheckSupportedFeatures(IEnumerable requiredFeatures)
+ => requiredFeatures.Where(f => !FeatureChecker.Supported(f)).ToArray();
}
diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs
index 03b42493..08f1f9df 100644
--- a/Penumbra/Api/Api/RedrawApi.cs
+++ b/Penumbra/Api/Api/RedrawApi.cs
@@ -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(GameObject? gameObject, RedrawType setting)
- => redrawService.RedrawObject(gameObject, setting);
+ public void RedrawObject(IGameObject? gameObject, RedrawType 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;
}
-}
+}
diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs
index ec57eba7..00a0c86f 100644
--- a/Penumbra/Api/Api/ResolveApi.cs
+++ b/Penumbra/Api/Api/ResolveApi.cs
@@ -1,5 +1,6 @@
using Dalamud.Plugin.Services;
using OtterGui.Services;
+using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Interop.PathResolving;
@@ -41,6 +42,19 @@ public class ResolveApi(
return ret.Select(r => r.ToString()).ToArray();
}
+ public PenumbraApiEc ResolvePath(Guid collectionId, string gamePath, out string resolvedPath)
+ {
+ resolvedPath = gamePath;
+ if (!collectionManager.Storage.ById(collectionId, out var collection))
+ return PenumbraApiEc.CollectionMissing;
+
+ if (!collection.HasCache)
+ return PenumbraApiEc.CollectionInactive;
+
+ resolvedPath = ResolvePath(gamePath, modManager, collection);
+ return PenumbraApiEc.Success;
+ }
+
public string[] ReverseResolvePlayerPath(string moddedPath)
{
if (!config.EnableMods)
@@ -64,6 +78,26 @@ public class ResolveApi(
return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
}
+ public PenumbraApiEc ResolvePaths(Guid collectionId, string[] forward, string[] reverse, out string[] resolvedForward,
+ out string[][] resolvedReverse)
+ {
+ resolvedForward = forward;
+ resolvedReverse = [];
+ if (!config.EnableMods)
+ return PenumbraApiEc.Success;
+
+ if (!collectionManager.Storage.ById(collectionId, out var collection))
+ return PenumbraApiEc.CollectionMissing;
+
+ if (!collection.HasCache)
+ return PenumbraApiEc.CollectionInactive;
+
+ resolvedForward = forward.Select(p => ResolvePath(p, modManager, collection)).ToArray();
+ var reverseResolved = collection.ReverseResolvePaths(reverse);
+ resolvedReverse = reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
+ return PenumbraApiEc.Success;
+ }
+
public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse)
{
if (!config.EnableMods)
@@ -94,7 +128,7 @@ public class ResolveApi(
if (!config.EnableMods)
return path;
- var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty;
+ var gamePath = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty;
var ret = collection.ResolvePath(gamePath);
return ret?.ToString() ?? path;
}
diff --git a/Penumbra/Api/Api/ResourceTreeApi.cs b/Penumbra/Api/Api/ResourceTreeApi.cs
index 6e9aaa48..dcec99bf 100644
--- a/Penumbra/Api/Api/ResourceTreeApi.cs
+++ b/Penumbra/Api/Api/ResourceTreeApi.cs
@@ -12,7 +12,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana
{
public Dictionary>?[] GetGameObjectResourcePaths(params ushort[] gameObjects)
{
- var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
+ var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0);
var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
@@ -28,7 +28,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana
public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData,
params ushort[] gameObjects)
{
- var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
+ var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
@@ -45,7 +45,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana
public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects)
{
- var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
+ var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs
index 38d080cc..7567acd3 100644
--- a/Penumbra/Api/Api/TemporaryApi.cs
+++ b/Penumbra/Api/Api/TemporaryApi.cs
@@ -1,10 +1,11 @@
-using OtterGui;
+using OtterGui.Log;
using OtterGui.Services;
using Penumbra.Api.Enums;
+using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
-using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.String.Classes;
@@ -15,10 +16,20 @@ public class TemporaryApi(
ObjectManager objects,
ActorManager actors,
CollectionManager collectionManager,
- TempModManager tempMods) : IPenumbraApiTemporary, IApiService
+ TempModManager tempMods,
+ 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)
@@ -62,7 +73,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
- if (!ConvertManips(manipString, out var m))
+ if (!MetaApi.ConvertManips(manipString, out var m, out _))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
@@ -88,7 +99,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
- if (!ConvertManips(manipString, out var m))
+ if (!MetaApi.ConvertManips(manipString, out var m, out _))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
@@ -127,6 +138,177 @@ public class TemporaryApi(
return ApiHelpers.Return(ret, args);
}
+ public (PenumbraApiEc, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId,
+ string modDirectory, string modName, int key)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName);
+ if (!collectionManager.Storage.ById(collectionId, out var collection))
+ return (ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args), null, string.Empty);
+
+ return QueryTemporaryModSettings(args, collection, modDirectory, modName, key);
+ }
+
+ public (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)? Settings, string Source)
+ QueryTemporaryModSettingsPlayer(int objectIndex,
+ string modDirectory, string modName, int key)
+ {
+ var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName);
+ if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
+ return (ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args), null, string.Empty);
+
+ return QueryTemporaryModSettings(args, collection, modDirectory, modName, key);
+ }
+
+ private (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)? Settings, string Source) QueryTemporaryModSettings(
+ in LazyString args, ModCollection collection, string modDirectory, string modName, int key)
+ {
+ if (!modManager.TryGetMod(modDirectory, modName, out var mod))
+ return (ApiHelpers.Return(PenumbraApiEc.ModMissing, args), null, string.Empty);
+
+ if (collection.Identity.Index <= 0)
+ return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty);
+
+ var settings = collection.GetTempSettings(mod.Index);
+ if (settings == null)
+ return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty);
+
+ if (settings.Lock > 0 && settings.Lock != key)
+ return (ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args), null, settings.Source);
+
+ return (ApiHelpers.Return(PenumbraApiEc.Success, args),
+ (settings.ForceInherit, settings.Enabled, settings.Priority.Value, settings.ConvertToShareable(mod).Settings), settings.Source);
+ }
+
+
+ public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled,
+ int priority,
+ IReadOnlyDictionary> options, string source, int key)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit,
+ "Enabled", enabled,
+ "Priority", priority, "Options", options, "Source", source, "Key", key);
+ if (!collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key);
+ }
+
+ public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled,
+ int priority,
+ IReadOnlyDictionary> options, string source, int key)
+ {
+ var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled",
+ enabled,
+ "Priority", priority, "Options", options, "Source", source, "Key", key);
+ if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key);
+ }
+
+ private PenumbraApiEc SetTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName,
+ bool inherit, bool enabled, int priority, IReadOnlyDictionary> options, string source, int key)
+ {
+ if (collection.Identity.Index <= 0)
+ return ApiHelpers.Return(PenumbraApiEc.TemporarySettingImpossible, args);
+
+ if (!modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
+
+ if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key))
+ if (collection.GetTempSettings(mod.Index) is { Lock: > 0 } oldSettings && oldSettings.Lock != key)
+ return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args);
+
+ var newSettings = new TemporaryModSettings()
+ {
+ ForceInherit = inherit,
+ Enabled = enabled,
+ Priority = new ModPriority(priority),
+ Lock = key,
+ Source = source,
+ Settings = SettingList.Default(mod),
+ };
+
+
+ foreach (var (groupName, optionNames) in options)
+ {
+ var ec = ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIdx, out var setting);
+ if (ec != PenumbraApiEc.Success)
+ return ApiHelpers.Return(ec, args);
+
+ newSettings.Settings[groupIdx] = setting;
+ }
+
+ if (collectionManager.Editor.SetTemporarySettings(collection, mod, newSettings, key))
+ return ApiHelpers.Return(PenumbraApiEc.Success, args);
+
+ // This should not happen since all error cases had been checked before.
+ return ApiHelpers.Return(PenumbraApiEc.UnknownError, args);
+ }
+
+ public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key);
+ if (!collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key);
+ }
+
+ public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key)
+ {
+ var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Key", key);
+ if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key);
+ }
+
+ private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key)
+ {
+ if (collection.Identity.Index <= 0)
+ return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
+
+ if (!modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
+
+ if (collection.GetTempSettings(mod.Index) is null)
+ return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
+
+ if (!collectionManager.Editor.SetTemporarySettings(collection, mod, null, key))
+ return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args);
+
+ return ApiHelpers.Return(PenumbraApiEc.Success, args);
+ }
+
+ public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key);
+ if (!collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ return RemoveAllTemporaryModSettings(args, collection, key);
+ }
+
+ public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key)
+ {
+ var args = ApiHelpers.Args("ObjectIndex", objectIndex, "Key", key);
+ if (!apiHelpers.AssociatedCollection(objectIndex, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ return RemoveAllTemporaryModSettings(args, collection, key);
+ }
+
+ private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key)
+ {
+ if (collection.Identity.Index <= 0)
+ return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args);
+
+ var numRemoved = collectionManager.Editor.ClearTemporarySettings(collection, key);
+ return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args);
+ }
+
+
///
/// Convert a dictionary of strings to a dictionary of game paths to full paths.
/// Only returns true if all paths can successfully be converted and added.
@@ -137,7 +319,7 @@ public class TemporaryApi(
paths = new Dictionary(redirections.Count);
foreach (var (gString, fString) in redirections)
{
- if (!Utf8GamePath.FromString(gString, out var path, false))
+ if (!Utf8GamePath.FromString(gString, out var path))
{
paths = null;
return false;
@@ -153,38 +335,4 @@ public class TemporaryApi(
return true;
}
-
- ///
- /// Convert manipulations from a transmitted base64 string to actual manipulations.
- /// The empty string is treated as an empty set.
- /// Only returns true if all conversions are successful and distinct.
- ///
- private static bool ConvertManips(string manipString,
- [NotNullWhen(true)] out HashSet? manips)
- {
- if (manipString.Length == 0)
- {
- manips = [];
- return true;
- }
-
- if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion)
- {
- manips = null;
- return false;
- }
-
- manips = new HashSet(manipArray!.Length);
- foreach (var manip in manipArray.Where(m => m.Validate()))
- {
- if (manips.Add(manip))
- continue;
-
- Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped.");
- manips = null;
- return false;
- }
-
- return true;
- }
}
diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs
index cf3cd8f2..6fb116f3 100644
--- a/Penumbra/Api/Api/UiApi.cs
+++ b/Penumbra/Api/Api/UiApi.cs
@@ -1,24 +1,28 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
-using Penumbra.GameData.Enums;
+using Penumbra.GameData.Data;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
+using Penumbra.UI.Integration;
+using Penumbra.UI.Tabs;
namespace Penumbra.Api.Api;
public class UiApi : IPenumbraApiUi, IApiService, IDisposable
{
- private readonly CommunicatorService _communicator;
- private readonly ConfigWindow _configWindow;
- private readonly ModManager _modManager;
+ private readonly CommunicatorService _communicator;
+ private readonly ConfigWindow _configWindow;
+ private readonly ModManager _modManager;
+ private readonly IntegrationSettingsRegistry _integrationSettings;
- public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager)
+ public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager, IntegrationSettingsRegistry integrationSettings)
{
- _communicator = communicator;
- _configWindow = configWindow;
- _modManager = modManager;
+ _communicator = communicator;
+ _configWindow = configWindow;
+ _modManager = modManager;
+ _integrationSettings = integrationSettings;
_communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
_communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
}
@@ -81,21 +85,29 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
public void CloseMainWindow()
=> _configWindow.IsOpen = false;
- private void OnChangedItemClick(MouseButton button, object? data)
+ private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data)
{
if (ChangedItemClicked == null)
return;
- var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
+ var (type, id) = data.ToApiObject();
ChangedItemClicked.Invoke(button, type, id);
}
- private void OnChangedItemHover(object? data)
+ private void OnChangedItemHover(IIdentifiedObjectData data)
{
if (ChangedItemTooltip == null)
return;
- var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
+ var (type, id) = data.ToApiObject();
ChangedItemTooltip.Invoke(type, id);
}
+
+ public PenumbraApiEc RegisterSettingsSection(Action draw)
+ => _integrationSettings.RegisterSection(draw);
+
+ public PenumbraApiEc UnregisterSettingsSection(Action draw)
+ => _integrationSettings.UnregisterSection(draw)
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.NothingChanged;
}
diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs
index 1c2cebcc..e10dc461 100644
--- a/Penumbra/Api/DalamudSubstitutionProvider.cs
+++ b/Penumbra/Api/DalamudSubstitutionProvider.cs
@@ -1,9 +1,9 @@
+using Dalamud.Interface;
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
-using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Services;
using Penumbra.String.Classes;
@@ -13,6 +13,7 @@ namespace Penumbra.Api;
public class DalamudSubstitutionProvider : IDisposable, IApiService
{
private readonly ITextureSubstitutionProvider _substitution;
+ private readonly IUiBuilder _uiBuilder;
private readonly ActiveCollectionData _activeCollectionData;
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
@@ -21,9 +22,10 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
=> _config.UseDalamudUiTextureRedirection;
public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData,
- Configuration config, CommunicatorService communicator)
+ Configuration config, CommunicatorService communicator, IUiBuilder ui)
{
_substitution = substitution;
+ _uiBuilder = ui;
_activeCollectionData = activeCollectionData;
_config = config;
_communicator = communicator;
@@ -41,6 +43,9 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
public void ResetSubstitutions(IEnumerable paths)
{
+ if (!_uiBuilder.UiPrepared)
+ return;
+
var transformed = paths
.Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8))
.Select(p => p.ToString());
@@ -91,10 +96,7 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
case ResolvedFileChanged.Type.Added:
case ResolvedFileChanged.Type.Removed:
case ResolvedFileChanged.Type.Replaced:
- ResetSubstitutions(new[]
- {
- key,
- });
+ ResetSubstitutions([key]);
break;
case ResolvedFileChanged.Type.FullRecomputeStart:
case ResolvedFileChanged.Type.FullRecomputeFinished:
@@ -127,7 +129,7 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
try
{
- if (!Utf8GamePath.FromString(path, out var utf8Path, true))
+ if (!Utf8GamePath.FromString(path, out var utf8Path))
return;
var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path);
diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs
index 859c46b4..79348a88 100644
--- a/Penumbra/Api/HttpApi.cs
+++ b/Penumbra/Api/HttpApi.cs
@@ -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();
- 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().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();
+ var data = await HttpContext.GetRequestDataAsync().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();
+ var data = await HttpContext.GetRequestDataAsync().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().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().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>? Settings)
+ {
+ public SetModSettingsData()
+ : this(null, string.Empty, string.Empty, null, null, null, null)
+ {}
+ }
}
}
diff --git a/Penumbra/Api/IpcLaunchingProvider.cs b/Penumbra/Api/IpcLaunchingProvider.cs
new file mode 100644
index 00000000..ff851003
--- /dev/null
+++ b/Penumbra/Api/IpcLaunchingProvider.cs
@@ -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}");
+ }
+ }
+}
diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs
index ebf71176..7cbe29f6 100644
--- a/Penumbra/Api/IpcProviders.cs
+++ b/Penumbra/Api/IpcProviders.cs
@@ -2,6 +2,8 @@ using Dalamud.Plugin;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Helpers;
+using Penumbra.Communication;
+using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
namespace Penumbra.Api;
@@ -9,11 +11,13 @@ public sealed class IpcProviders : IDisposable, IApiService
{
private readonly List _providers;
- private readonly EventProvider _disposedProvider;
- private readonly EventProvider _initializedProvider;
+ private readonly EventProvider _disposedProvider;
+ private readonly EventProvider _initializedProvider;
+ private readonly CharacterUtility _characterUtility;
- public IpcProviders(DalamudPluginInterface pi, IPenumbraApi api)
+ public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api, CharacterUtility characterUtility)
{
+ _characterUtility = characterUtility;
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
_providers =
@@ -25,6 +29,7 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection),
IpcSubscribers.SetCollection.Provider(pi, api.Collection),
IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection),
+ IpcSubscribers.CheckCurrentChangedItemFunc.Provider(pi, api.Collection),
IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing),
IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing),
@@ -35,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),
@@ -47,11 +54,19 @@ 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),
+ IpcSubscribers.GetChangedItemAdapterDictionary.Provider(pi, api.Mods),
+ IpcSubscribers.GetChangedItemAdapterList.Provider(pi, api.Mods),
IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings),
IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings),
+ IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings),
+ IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings),
+ IpcSubscribers.GetSettingsInAllCollections.Provider(pi, api.ModSettings),
IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings),
IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings),
@@ -62,16 +77,19 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ApiVersion.Provider(pi, api),
new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility
- new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility
+ new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility
IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState),
IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState),
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
+ IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
+ IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
+ IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
@@ -81,6 +99,8 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve),
IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve),
+ IpcSubscribers.ResolvePath.Provider(pi, api.Resolve),
+ IpcSubscribers.ResolvePaths.Provider(pi, api.Resolve),
IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree),
IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree),
@@ -96,6 +116,14 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary),
IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary),
+ IpcSubscribers.SetTemporaryModSettings.Provider(pi, api.Temporary),
+ IpcSubscribers.SetTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
+ IpcSubscribers.RemoveTemporaryModSettings.Provider(pi, api.Temporary),
+ IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
+ IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary),
+ IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
+ IpcSubscribers.QueryTemporaryModSettings.Provider(pi, api.Temporary),
+ IpcSubscribers.QueryTemporaryModSettingsPlayer.Provider(pi, api.Temporary),
IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui),
IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui),
@@ -105,12 +133,24 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui),
IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
+ IpcSubscribers.RegisterSettingsSection.Provider(pi, api.Ui),
+ IpcSubscribers.UnregisterSettingsSection.Provider(pi, api.Ui),
];
+ if (_characterUtility.Ready)
+ _initializedProvider.Invoke();
+ else
+ _characterUtility.LoadingFinished.Subscribe(OnCharacterUtilityReady, CharacterUtilityFinished.Priority.IpcProvider);
+ }
+
+ private void OnCharacterUtilityReady()
+ {
_initializedProvider.Invoke();
+ _characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady);
}
public void Dispose()
{
+ _characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady);
foreach (var provider in _providers)
provider.Dispose();
_providers.Clear();
diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs
index 2679bc69..f033b7c3 100644
--- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs
+++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs
@@ -1,26 +1,26 @@
+using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
-using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
-using Penumbra.GameData.Enums;
+using Penumbra.GameData.Data;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Penumbra.Api.IpcTester;
-public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService
+public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
{
private int _objectIdx;
private string _collectionIdString = string.Empty;
- private Guid? _collectionId = null;
- private bool _allowCreation = true;
- private bool _allowDeletion = true;
- private ApiCollectionType _type = ApiCollectionType.Yourself;
+ private Guid? _collectionId;
+ private bool _allowCreation = true;
+ private bool _allowDeletion = true;
+ private ApiCollectionType _type = ApiCollectionType.Yourself;
private Dictionary _collections = [];
private (string, ChangedItemType, uint)[] _changedItems = [];
@@ -116,11 +116,15 @@ public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService
var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
_changedItems = items.Select(kvp =>
{
- var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value);
+ var (type, id) = kvp.Value.ToApiObject();
return (kvp.Key, type, id);
}).ToArray();
ImGui.OpenPopup("Changed Item List");
}
+ IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
+ if (ImGui.Button("Redraw##ObjectCollection"))
+ new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
+
}
private void DrawChangedItemPopup()
@@ -130,9 +134,9 @@ public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService
if (!p)
return;
- using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
+ using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
{
- if (t)
+ if (table)
ImGuiClip.ClippedDraw(_changedItems, t =>
{
ImGuiUtil.DrawTableColumn(t.Item1);
diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs
index 94b1e4e8..d754cf90 100644
--- a/Penumbra/Api/IpcTester/EditingIpcTester.cs
+++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs
@@ -1,5 +1,5 @@
+using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
-using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
@@ -8,7 +8,7 @@ using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Api.IpcTester;
-public class EditingIpcTester(DalamudPluginInterface pi) : IUiService
+public class EditingIpcTester(IDalamudPluginInterface pi) : IUiService
{
private string _inputPath = string.Empty;
private string _inputPath2 = string.Empty;
diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs
index 93806162..38a09714 100644
--- a/Penumbra/Api/IpcTester/GameStateIpcTester.cs
+++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs
@@ -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;
@@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester;
public class GameStateIpcTester : IUiService, IDisposable
{
- private readonly DalamudPluginInterface _pi;
+ private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber CharacterBaseCreating;
public readonly EventSubscriber CharacterBaseCreated;
public readonly EventSubscriber GameObjectResourcePathResolved;
@@ -30,7 +30,7 @@ public class GameStateIpcTester : IUiService, IDisposable
private int _currentCutsceneParent;
private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success;
- public GameStateIpcTester(DalamudPluginInterface pi)
+ public GameStateIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated);
@@ -134,7 +134,6 @@ public class GameStateIpcTester : IUiService, IDisposable
private static unsafe string GetObjectName(nint gameObject)
{
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject;
- var name = obj != null ? obj->Name : null;
- return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown";
+ return obj != null && obj->Name[0] != 0 ? new ByteString(obj->Name).ToString() : "Unknown";
}
}
diff --git a/Penumbra/Api/IpcTester/IpcTester.cs b/Penumbra/Api/IpcTester/IpcTester.cs
index 201e7068..b03d7e03 100644
--- a/Penumbra/Api/IpcTester/IpcTester.cs
+++ b/Penumbra/Api/IpcTester/IpcTester.cs
@@ -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;
diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs
index 3fa7de7f..bee1981c 100644
--- a/Penumbra/Api/IpcTester/MetaIpcTester.cs
+++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs
@@ -1,14 +1,20 @@
+using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
-using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
+using OtterGui.Text;
+using Penumbra.Api.Api;
using Penumbra.Api.IpcSubscribers;
+using Penumbra.Meta.Manipulations;
namespace Penumbra.Api.IpcTester;
-public class MetaIpcTester(DalamudPluginInterface pi) : IUiService
+public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService
{
- private int _gameObjectIndex;
+ private int _gameObjectIndex;
+ private string _metaBase64 = string.Empty;
+ private MetaDictionary _metaDict = new();
+ private byte _parsedVersion = byte.MaxValue;
public void Draw()
{
@@ -17,6 +23,11 @@ public class MetaIpcTester(DalamudPluginInterface pi) : IUiService
return;
ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0);
+ if (ImUtf8.InputText("##metaText"u8, ref _metaBase64, "Base64 Metadata..."u8))
+ if (!MetaApi.ConvertManips(_metaBase64, out _metaDict!, out _parsedVersion))
+ _metaDict ??= new MetaDictionary();
+
+
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
@@ -34,5 +45,8 @@ public class MetaIpcTester(DalamudPluginInterface pi) : IUiService
var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex);
ImGui.SetClipboardText(base64);
}
+
+ IpcTester.DrawIntro(string.Empty, "Parsed Data");
+ ImUtf8.Text($"Version: {_parsedVersion}, Count: {_metaDict.Count}");
}
}
diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs
index b117d603..152efa45 100644
--- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs
+++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs
@@ -1,8 +1,9 @@
+using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
-using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
+using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
@@ -12,7 +13,7 @@ namespace Penumbra.Api.IpcTester;
public class ModSettingsIpcTester : IUiService, IDisposable
{
- private readonly DalamudPluginInterface _pi;
+ private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber SettingChanged;
private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
@@ -22,18 +23,22 @@ public class ModSettingsIpcTester : IUiService, IDisposable
private bool _lastSettingChangeInherited;
private DateTimeOffset _lastSettingChange;
- private string _settingsModDirectory = string.Empty;
- private string _settingsModName = string.Empty;
- private Guid? _settingsCollection;
- private string _settingsCollectionName = string.Empty;
- private bool _settingsIgnoreInheritance;
- private bool _settingsInherit;
- private bool _settingsEnabled;
- private int _settingsPriority;
- private IReadOnlyDictionary? _availableSettings;
- private Dictionary>? _currentSettings;
+ private string _settingsModDirectory = string.Empty;
+ private string _settingsModName = string.Empty;
+ private Guid? _settingsCollection;
+ private string _settingsCollectionName = string.Empty;
+ private bool _settingsIgnoreInheritance;
+ private bool _settingsIgnoreTemporary;
+ private int _settingsKey;
+ private bool _settingsInherit;
+ private bool _settingsTemporary;
+ private bool _settingsEnabled;
+ private int _settingsPriority;
+ private IReadOnlyDictionary? _availableSettings;
+ private Dictionary>? _currentSettings;
+ private Dictionary>, bool, bool)>? _allSettings;
- public ModSettingsIpcTester(DalamudPluginInterface pi)
+ public ModSettingsIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting);
@@ -54,7 +59,9 @@ public class ModSettingsIpcTester : IUiService, IDisposable
ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100);
ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100);
ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName);
- ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance);
+ ImUtf8.Checkbox("Ignore Inheritance"u8, ref _settingsIgnoreInheritance);
+ ImUtf8.Checkbox("Ignore Temporary"u8, ref _settingsIgnoreTemporary);
+ ImUtf8.InputScalar("Key"u8, ref _settingsKey);
var collection = _settingsCollection.GetValueOrDefault(Guid.Empty);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
@@ -83,10 +90,11 @@ public class ModSettingsIpcTester : IUiService, IDisposable
_lastSettingsError = ret.Item1;
if (ret.Item1 == PenumbraApiEc.Success)
{
- _settingsEnabled = ret.Item2?.Item1 ?? false;
- _settingsInherit = ret.Item2?.Item4 ?? true;
- _settingsPriority = ret.Item2?.Item2 ?? 0;
- _currentSettings = ret.Item2?.Item3;
+ _settingsEnabled = ret.Item2?.Item1 ?? false;
+ _settingsInherit = ret.Item2?.Item4 ?? true;
+ _settingsTemporary = false;
+ _settingsPriority = ret.Item2?.Item2 ?? 0;
+ _currentSettings = ret.Item2?.Item3;
}
else
{
@@ -94,6 +102,40 @@ public class ModSettingsIpcTester : IUiService, IDisposable
}
}
+ IpcTester.DrawIntro(GetCurrentModSettingsWithTemp.Label, "Get Current Settings With Temp");
+ if (ImGui.Button("Get##CurrentTemp"))
+ {
+ var ret = new GetCurrentModSettingsWithTemp(_pi)
+ .Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey);
+ _lastSettingsError = ret.Item1;
+ if (ret.Item1 == PenumbraApiEc.Success)
+ {
+ _settingsEnabled = ret.Item2?.Item1 ?? false;
+ _settingsInherit = ret.Item2?.Item4 ?? true;
+ _settingsTemporary = ret.Item2?.Item5 ?? false;
+ _settingsPriority = ret.Item2?.Item2 ?? 0;
+ _currentSettings = ret.Item2?.Item3;
+ }
+ else
+ {
+ _currentSettings = null;
+ }
+ }
+
+ IpcTester.DrawIntro(GetAllModSettings.Label, "Get All Mod Settings");
+ if (ImGui.Button("Get##All"))
+ {
+ var ret = new GetAllModSettings(_pi).Invoke(collection, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey);
+ _lastSettingsError = ret.Item1;
+ _allSettings = ret.Item2;
+ }
+
+ if (_allSettings != null)
+ {
+ ImGui.SameLine();
+ ImUtf8.Text($"{_allSettings.Count} Mods");
+ }
+
IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod");
ImGui.Checkbox("##inherit", ref _settingsInherit);
ImGui.SameLine();
diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs
index 43f397e5..9ea53366 100644
--- a/Penumbra/Api/IpcTester/ModsIpcTester.cs
+++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs
@@ -1,8 +1,9 @@
+using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
-using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
+using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
@@ -11,18 +12,19 @@ namespace Penumbra.Api.IpcTester;
public class ModsIpcTester : IUiService, IDisposable
{
- private readonly DalamudPluginInterface _pi;
+ private readonly IDalamudPluginInterface _pi;
- private string _modDirectory = string.Empty;
- private string _modName = string.Empty;
- private string _pathInput = string.Empty;
- private string _newInstallPath = string.Empty;
- private PenumbraApiEc _lastReloadEc;
- private PenumbraApiEc _lastAddEc;
- private PenumbraApiEc _lastDeleteEc;
- private PenumbraApiEc _lastSetPathEc;
- private PenumbraApiEc _lastInstallEc;
- private Dictionary _mods = [];
+ private string _modDirectory = string.Empty;
+ private string _modName = string.Empty;
+ private string _pathInput = string.Empty;
+ private string _newInstallPath = string.Empty;
+ private PenumbraApiEc _lastReloadEc;
+ private PenumbraApiEc _lastAddEc;
+ private PenumbraApiEc _lastDeleteEc;
+ private PenumbraApiEc _lastSetPathEc;
+ private PenumbraApiEc _lastInstallEc;
+ private Dictionary _mods = [];
+ private Dictionary _changedItems = [];
public readonly EventSubscriber DeleteSubscriber;
public readonly EventSubscriber AddSubscriber;
@@ -36,7 +38,7 @@ public class ModsIpcTester : IUiService, IDisposable
private string _lastMovedModFrom = string.Empty;
private string _lastMovedModTo = string.Empty;
- public ModsIpcTester(DalamudPluginInterface pi)
+ public ModsIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
DeleteSubscriber = ModDeleted.Subscriber(pi, s =>
@@ -120,6 +122,14 @@ public class ModsIpcTester : IUiService, IDisposable
ImGui.SameLine();
ImGui.TextUnformatted(_lastDeleteEc.ToString());
+ IpcTester.DrawIntro(GetChangedItems.Label, "Get Changed Items");
+ DrawChangedItemsPopup();
+ if (ImUtf8.Button("Get##ChangedItems"u8))
+ {
+ _changedItems = new GetChangedItems(_pi).Invoke(_modDirectory, _modName);
+ ImUtf8.OpenPopup("ChangedItems"u8);
+ }
+
IpcTester.DrawIntro(GetModPath.Label, "Current Path");
var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName);
ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]");
@@ -157,4 +167,18 @@ public class ModsIpcTester : IUiService, IDisposable
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
ImGui.CloseCurrentPopup();
}
+
+ private void DrawChangedItemsPopup()
+ {
+ ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
+ using var p = ImUtf8.Popup("ChangedItems"u8);
+ if (!p)
+ return;
+
+ foreach (var (name, data) in _changedItems)
+ ImUtf8.Text($"{name}: {data}");
+
+ if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused())
+ ImGui.CloseCurrentPopup();
+ }
}
diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs
index 984f17b1..073305d0 100644
--- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs
+++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs
@@ -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 DalamudPluginInterface _pi;
+ private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber ModDirectoryChanged;
public readonly EventSubscriber Initialized;
public readonly EventSubscriber Disposed;
@@ -26,10 +27,13 @@ public class PluginStateIpcTester : IUiService, IDisposable
private readonly List _initializedList = [];
private readonly List _disposedList = [];
+ private string _requiredFeatureString = string.Empty;
+ private string[] _requiredFeatures = [];
+
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
private bool? _lastEnabledValue;
- public PluginStateIpcTester(DalamudPluginInterface pi)
+ public PluginStateIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged);
@@ -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"))
diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs
index 801f0b97..6b853ed2 100644
--- a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs
+++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs
@@ -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;
@@ -13,14 +13,14 @@ namespace Penumbra.Api.IpcTester;
public class RedrawingIpcTester : IUiService, IDisposable
{
- private readonly DalamudPluginInterface _pi;
+ private readonly IDalamudPluginInterface _pi;
private readonly ObjectManager _objects;
public readonly EventSubscriber Redrawn;
private int _redrawIndex;
private string _lastRedrawnString = "None";
- public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects)
+ public RedrawingIpcTester(IDalamudPluginInterface pi, ObjectManager objects)
{
_pi = pi;
_objects = objects;
diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs
index 978ed8d6..9fc5bfc7 100644
--- a/Penumbra/Api/IpcTester/ResolveIpcTester.cs
+++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs
@@ -1,5 +1,5 @@
using Dalamud.Plugin;
-using ImGuiNET;
+using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.IpcSubscribers;
@@ -7,7 +7,7 @@ using Penumbra.String.Classes;
namespace Penumbra.Api.IpcTester;
-public class ResolveIpcTester(DalamudPluginInterface pi) : IUiService
+public class ResolveIpcTester(IDalamudPluginInterface pi) : IUiService
{
private string _currentResolvePath = string.Empty;
private string _currentReversePath = string.Empty;
diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs
index 1f57fc9d..e6c8d52e 100644
--- a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs
+++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs
@@ -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;
@@ -15,7 +16,7 @@ using Penumbra.GameData.Structs;
namespace Penumbra.Api.IpcTester;
-public class ResourceTreeIpcTester(DalamudPluginInterface pi, ObjectManager objects) : IUiService
+public class ResourceTreeIpcTester(IDalamudPluginInterface pi, ObjectManager objects) : IUiService
{
private readonly Stopwatch _stopwatch = new();
diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs
index a8405eb2..d46c5728 100644
--- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs
+++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs
@@ -1,13 +1,15 @@
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;
+using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
-using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
@@ -15,7 +17,7 @@ using Penumbra.Services;
namespace Penumbra.Api.IpcTester;
public class TemporaryIpcTester(
- DalamudPluginInterface pi,
+ IDalamudPluginInterface pi,
ModManager modManager,
CollectionManager collections,
TempModManager tempMods,
@@ -26,13 +28,17 @@ public class TemporaryIpcTester(
{
public Guid LastCreatedCollectionId = Guid.Empty;
+ private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9;
+
private Guid? _tempGuid;
private string _tempCollectionName = string.Empty;
private string _tempCollectionGuidName = string.Empty;
private string _tempModName = string.Empty;
+ private string _modDirectory = string.Empty;
private string _tempGamePath = "test/game/path.mtrl";
private string _tempFilePath = "test/success.mtrl";
private string _tempManipulation = string.Empty;
+ private string _identity = string.Empty;
private PenumbraApiEc _lastTempError;
private int _tempActorIndex;
private bool _forceOverwrite;
@@ -43,13 +49,15 @@ 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);
- ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
- ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
- ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
- ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256);
+ ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
+ ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256);
+ ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
+ ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
+ ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
@@ -67,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;
@@ -100,9 +108,8 @@ public class TemporaryIpcTester(
!collections.Storage.ByName(_tempModName, out var copyCollection))
&& copyCollection is { HasCache: true })
{
- var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
- var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(),
- MetaManipulation.CurrentVersion);
+ var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
+ var manips = MetaApi.CompressMetaManipulations(copyCollection);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
}
@@ -119,15 +126,124 @@ public class TemporaryIpcTester(
IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections");
if (ImGui.Button("Remove##ModAll"))
_lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue);
+
+ IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection");
+ if (ImUtf8.Button("Set##SetTemporary"u8))
+ _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337,
+ new Dictionary>(),
+ "IPC Tester", 1337);
+
+ IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection");
+ if (ImUtf8.Button("Set##SetTemporaryPlayer"u8))
+ _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337,
+ new Dictionary>(),
+ "IPC Tester", 1337);
+
+ IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection");
+ if (ImUtf8.Button("Remove##RemoveTemporary"u8))
+ _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1337);
+ ImGui.SameLine();
+ if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8))
+ _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1338);
+
+ IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection");
+ if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8))
+ _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1337);
+ ImGui.SameLine();
+ if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8))
+ _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1338);
+
+ IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection");
+ if (ImUtf8.Button("Remove##RemoveAllTemporary"u8))
+ _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1337);
+ ImGui.SameLine();
+ if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporary"u8))
+ _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1338);
+
+ IpcTester.DrawIntro(RemoveAllTemporaryModSettingsPlayer.Label, "Remove All Temporary Mod Settings from game object Collection");
+ if (ImUtf8.Button("Remove##RemoveAllTemporaryPlayer"u8))
+ _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1337);
+ ImGui.SameLine();
+ if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8))
+ _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338);
+
+ IpcTester.DrawIntro(QueryTemporaryModSettings.Label, "Query Temporary Mod Settings from specific Collection");
+ ImUtf8.Button("Query##QueryTemporaryModSettings"u8);
+ if (ImGui.IsItemHovered())
+ {
+ _lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1337);
+ DrawTooltip(settings, source);
+ }
+
+ ImGui.SameLine();
+ ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporary"u8);
+ if (ImGui.IsItemHovered())
+ {
+ _lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1338);
+ DrawTooltip(settings, source);
+ }
+
+ IpcTester.DrawIntro(QueryTemporaryModSettingsPlayer.Label, "Query Temporary Mod Settings from game object Collection");
+ ImUtf8.Button("Query##QueryTemporaryModSettingsPlayer"u8);
+ if (ImGui.IsItemHovered())
+ {
+ _lastTempError =
+ new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1337);
+ DrawTooltip(settings, source);
+ }
+
+ ImGui.SameLine();
+ ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporaryPlayer"u8);
+ if (ImGui.IsItemHovered())
+ {
+ _lastTempError =
+ new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1338);
+ DrawTooltip(settings, source);
+ }
+
+ void DrawTooltip((bool ForceInherit, bool Enabled, int Priority, Dictionary> Settings)? settings, string source)
+ {
+ using var tt = ImUtf8.Tooltip();
+ ImUtf8.Text($"Query returned {_lastTempError}");
+ if (settings != null)
+ ImUtf8.Text($"Settings created by {(source.Length == 0 ? "Unknown Source" : source)}:");
+ else
+ ImUtf8.Text(source.Length > 0 ? $"Locked by {source}." : "No settings exist.");
+ ImGui.Separator();
+ if (settings == null)
+ {
+
+ return;
+ }
+
+ using (ImUtf8.Group())
+ {
+ ImUtf8.Text("Force Inherit"u8);
+ ImUtf8.Text("Enabled"u8);
+ ImUtf8.Text("Priority"u8);
+ foreach (var group in settings.Value.Settings.Keys)
+ ImUtf8.Text(group);
+ }
+
+ ImGui.SameLine();
+ using (ImUtf8.Group())
+ {
+ ImUtf8.Text($"{settings.Value.ForceInherit}");
+ ImUtf8.Text($"{settings.Value.Enabled}");
+ ImUtf8.Text($"{settings.Value.Priority}");
+ foreach (var group in settings.Value.Settings.Values)
+ ImUtf8.Text(string.Join("; ", group));
+ }
+ }
}
public void DrawCollections()
{
- using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
+ using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8);
if (!collTree)
return;
- using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit);
+ using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
@@ -138,16 +254,16 @@ public class TemporaryIpcTester(
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
.FirstOrDefault()
?? "Unknown";
- if (ImGui.Button("Save##Collection"))
+ if (_debug && ImUtf8.Button("Save##Collection"u8))
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TableNextColumn();
- ImGuiUtil.CopyOnClickSelectable(collection.Identifier);
+ ImGuiUtil.CopyOnClickSelectable(collection.Identity.Identifier);
}
- ImGuiUtil.DrawTableColumn(collection.Name);
+ ImGuiUtil.DrawTableColumn(collection.Identity.Name);
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
ImGuiUtil.DrawTableColumn(string.Join(", ",
@@ -168,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();
@@ -187,8 +303,8 @@ public class TemporaryIpcTester(
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
- foreach (var manip in mod.Default.Manipulations)
- ImGui.TextUnformatted(manip.ToString());
+ foreach (var identifier in mod.Default.Manipulations.Identifiers)
+ ImGui.TextUnformatted(identifier.ToString());
}
}
}
@@ -197,7 +313,7 @@ public class TemporaryIpcTester(
{
PrintList("All", tempMods.ModsForAllCollections);
foreach (var (collection, list) in tempMods.Mods)
- PrintList(collection.Name, list);
+ PrintList(collection.Identity.Name, list);
}
}
}
diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs
index a2c36938..852339c9 100644
--- a/Penumbra/Api/IpcTester/UiIpcTester.cs
+++ b/Penumbra/Api/IpcTester/UiIpcTester.cs
@@ -1,5 +1,5 @@
using Dalamud.Plugin;
-using ImGuiNET;
+using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
@@ -10,7 +10,7 @@ namespace Penumbra.Api.IpcTester;
public class UiIpcTester : IUiService, IDisposable
{
- private readonly DalamudPluginInterface _pi;
+ private readonly IDalamudPluginInterface _pi;
public readonly EventSubscriber PreSettingsTabBar;
public readonly EventSubscriber PreSettingsPanel;
public readonly EventSubscriber PostEnabled;
@@ -28,7 +28,7 @@ public class UiIpcTester : IUiService, IDisposable
private string _modName = string.Empty;
private PenumbraApiEc _ec = PenumbraApiEc.Success;
- public UiIpcTester(DalamudPluginInterface pi)
+ public UiIpcTester(IDalamudPluginInterface pi)
{
_pi = pi;
PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod);
diff --git a/Penumbra/Api/ModChangedItemAdapter.cs b/Penumbra/Api/ModChangedItemAdapter.cs
new file mode 100644
index 00000000..8d2d473c
--- /dev/null
+++ b/Penumbra/Api/ModChangedItemAdapter.cs
@@ -0,0 +1,103 @@
+using Penumbra.GameData.Data;
+using Penumbra.Mods.Manager;
+
+namespace Penumbra.Api;
+
+public sealed class ModChangedItemAdapter(WeakReference storage)
+ : IReadOnlyDictionary>,
+ IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)>
+{
+ IEnumerator<(string ModDirectory, IReadOnlyDictionary ChangedItems)>
+ IEnumerable<(string ModDirectory, IReadOnlyDictionary ChangedItems)>.GetEnumerator()
+ => Storage.Select(m => (m.Identifier, (IReadOnlyDictionary)new ChangedItemDictionaryAdapter(m.ChangedItems)))
+ .GetEnumerator();
+
+ public IEnumerator>> GetEnumerator()
+ => Storage.Select(m => new KeyValuePair>(m.Identifier,
+ new ChangedItemDictionaryAdapter(m.ChangedItems)))
+ .GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+
+ public int Count
+ => Storage.Count;
+
+ public bool ContainsKey(string key)
+ => Storage.TryGetMod(key, string.Empty, out _);
+
+ public bool TryGetValue(string key, [NotNullWhen(true)] out IReadOnlyDictionary? value)
+ {
+ if (Storage.TryGetMod(key, string.Empty, out var mod))
+ {
+ value = new ChangedItemDictionaryAdapter(mod.ChangedItems);
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
+ public IReadOnlyDictionary this[string key]
+ => TryGetValue(key, out var v) ? v : throw new KeyNotFoundException();
+
+ (string ModDirectory, IReadOnlyDictionary ChangedItems)
+ IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)>.this[int index]
+ {
+ get
+ {
+ var m = Storage[index];
+ return (m.Identifier, new ChangedItemDictionaryAdapter(m.ChangedItems));
+ }
+ }
+
+ public IEnumerable Keys
+ => Storage.Select(m => m.Identifier);
+
+ public IEnumerable> Values
+ => Storage.Select(m => new ChangedItemDictionaryAdapter(m.ChangedItems));
+
+ private ModStorage Storage
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ get => storage.TryGetTarget(out var t)
+ ? t
+ : throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed.");
+ }
+
+ private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary
+ {
+ public IEnumerator> GetEnumerator()
+ => data.Select(d => new KeyValuePair(d.Key, d.Value?.ToInternalObject())).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+
+ public int Count
+ => data.Count;
+
+ public bool ContainsKey(string key)
+ => data.ContainsKey(key);
+
+ public bool TryGetValue(string key, out object? value)
+ {
+ if (data.TryGetValue(key, out var v))
+ {
+ value = v?.ToInternalObject();
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
+ public object? this[string key]
+ => data[key]?.ToInternalObject();
+
+ public IEnumerable Keys
+ => data.Keys;
+
+ public IEnumerable
public sealed class CollectionCache : IDisposable
{
- private readonly CollectionCacheManager _manager;
- private readonly ModCollection _collection;
- public readonly CollectionModData ModData = new();
- private readonly SortedList, object?)> _changedItems = [];
- public readonly ConcurrentDictionary ResolvedFiles = new();
- public readonly CustomResourceCache CustomResources;
- public readonly MetaCache Meta;
- public readonly Dictionary> ConflictDict = [];
+ private readonly CollectionCacheManager _manager;
+ private readonly ModCollection _collection;
+ public readonly CollectionModData ModData = new();
+ private readonly SortedList, IIdentifiedObjectData)> _changedItems = [];
+ public readonly ConcurrentDictionary ResolvedFiles = new();
+ public readonly CustomResourceCache CustomResources;
+ public readonly MetaCache Meta;
+ public readonly Dictionary> ConflictDict = [];
public int Calculating = -1;
public string AnonymizedName
- => _collection.AnonymizedName;
+ => _collection.Identity.AnonymizedName;
public IEnumerable> AllConflicts
=> ConflictDict.Values;
@@ -41,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, object?)> ChangedItems
+ public IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems
{
get
{
@@ -125,12 +127,6 @@ public sealed class CollectionCache : IDisposable
return ret;
}
- public void ForceFile(Utf8GamePath path, FullPath fullPath)
- => _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath));
-
- public void RemovePath(Utf8GamePath path)
- => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty));
-
public void ReloadMod(IMod mod, bool addMetaChanges)
=> _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges));
@@ -182,7 +178,7 @@ public sealed class CollectionCache : IDisposable
var (paths, manipulations) = ModData.RemoveMod(mod);
if (addMetaChanges)
- _collection.IncrementCounter();
+ _collection.Counters.IncrementChange();
foreach (var path in paths)
{
@@ -233,15 +229,33 @@ public sealed class CollectionCache : IDisposable
foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod);
- foreach (var manip in files.Manipulations)
- AddManipulation(manip, mod);
+ if (files.Manipulations.Count > 0)
+ {
+ foreach (var (identifier, entry) in files.Manipulations.Eqp)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Eqdp)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Est)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Gmp)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Rsp)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Imc)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Atch)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Shp)
+ AddManipulation(mod, identifier, entry);
+ foreach (var (identifier, entry) in files.Manipulations.Atr)
+ AddManipulation(mod, identifier, entry);
+ foreach (var identifier in files.Manipulations.GlobalEqp)
+ AddManipulation(mod, identifier, null!);
+ }
if (addMetaChanges)
{
- _collection.IncrementCounter();
- if (mod.TotalManipulations > 0)
- AddMetaFiles(false);
-
+ _collection.Counters.IncrementChange();
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
@@ -251,7 +265,7 @@ public sealed class CollectionCache : IDisposable
if (mod.Index < 0)
return mod.GetData();
- var settings = _collection[mod.Index].Settings;
+ var settings = _collection.GetActualSettings(mod.Index).Settings;
return settings is not { Enabled: true }
? AppliedModData.Empty
: mod.GetData(settings);
@@ -265,6 +279,24 @@ public sealed class CollectionCache : IDisposable
_manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod);
}
+ private static bool IsRedirectionSupported(Utf8GamePath path, IMod mod)
+ {
+ var ext = path.Extension().AsciiToLower().ToString();
+ switch (ext)
+ {
+ case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc":
+ Penumbra.Messager.NotificationMessage(
+ $"Redirection of {ext} files for {mod.Name} is unsupported. This probably means that the mod is outdated and may not work correctly.\n\nPlease tell the mod creator to use the corresponding meta manipulations instead.",
+ NotificationType.Warning);
+ return false;
+ case ".lvb" or ".lgb" or ".sgb":
+ Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.\n\nThis mod will probably not work correctly.",
+ NotificationType.Warning);
+ return false;
+ default: return true;
+ }
+ }
+
// Add a specific file redirection, handling potential conflicts.
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
@@ -274,6 +306,9 @@ public sealed class CollectionCache : IDisposable
if (!CheckFullPath(path, file))
return;
+ if (!IsRedirectionSupported(path, mod))
+ return;
+
try
{
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
@@ -333,8 +368,9 @@ public sealed class CollectionCache : IDisposable
// Returns if the added mod takes priority before the existing mod.
private bool AddConflict(object data, IMod addedMod, IMod existingMod)
{
- var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority;
- var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority;
+ var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority;
+ var existingPriority =
+ existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority;
if (existingPriority < addedPriority)
{
@@ -342,7 +378,7 @@ public sealed class CollectionCache : IDisposable
foreach (var conflict in tmpConflicts)
{
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0
- || data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0)
+ || data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0)
AddConflict(data, addedMod, conflict.Mod2);
}
@@ -374,12 +410,12 @@ public sealed class CollectionCache : IDisposable
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
- private void AddManipulation(MetaManipulation manip, IMod mod)
+ private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry)
{
- if (!Meta.TryGetValue(manip, out var existingMod))
+ if (!Meta.TryGetMod(identifier, out var existingMod))
{
- Meta.ApplyMod(manip, mod);
- ModData.AddManip(mod, manip);
+ Meta.ApplyMod(mod, identifier, entry);
+ ModData.AddManip(mod, identifier);
return;
}
@@ -387,34 +423,29 @@ public sealed class CollectionCache : IDisposable
if (mod == existingMod)
return;
- if (AddConflict(manip, mod, existingMod))
+ if (AddConflict(identifier, mod, existingMod))
{
- ModData.RemoveManip(existingMod, manip);
- Meta.ApplyMod(manip, mod);
- ModData.AddManip(mod, manip);
+ ModData.RemoveManip(existingMod, identifier);
+ Meta.ApplyMod(mod, identifier, entry);
+ ModData.AddManip(mod, identifier);
}
}
- // Add all necessary meta file redirects.
- public void AddMetaFiles(bool fromFullCompute)
- => Meta.SetImcFiles(fromFullCompute);
-
-
// Identify and record all manipulated objects for this entire collection.
private void SetChangedItems()
{
- if (_changedItemsSaveCounter == _collection.ChangeCounter)
+ if (_changedItemsSaveCounter == _collection.Counters.Change)
return;
try
{
- _changedItemsSaveCounter = _collection.ChangeCounter;
+ _changedItemsSaveCounter = _collection.Counters.Change;
_changedItems.Clear();
// Skip IMCs because they would result in far too many false-positive items,
// since they are per set instead of per item-slot/item/variant.
var identifier = _manager.MetaFileManager.Identifier;
- var items = new SortedList(512);
+ var items = new SortedList(512);
void AddItems(IMod mod)
{
@@ -423,8 +454,9 @@ public sealed class CollectionCache : IDisposable
if (!_changedItems.TryGetValue(name, out var data))
_changedItems.Add(name, (new SingleArray(mod), obj));
else if (!data.Item1.Contains(mod))
- _changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj);
- else if (obj is int x && data.Item2 is int y)
+ _changedItems[name] = (data.Item1.Append(mod),
+ obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
+ else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y)
_changedItems[name] = (data.Item1, x + y);
}
@@ -437,9 +469,9 @@ public sealed class CollectionCache : IDisposable
AddItems(modPath.Mod);
}
- foreach (var (manip, mod) in Meta)
+ foreach (var (manip, mod) in Meta.IdentifierSources)
{
- identifier.MetaChangedItems(items, manip);
+ manip.AddChangedItems(identifier, items);
AddItems(mod);
}
diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs
index ae424b94..ec48e608 100644
--- a/Penumbra/Collections/Cache/CollectionCacheManager.cs
+++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs
@@ -1,10 +1,11 @@
using Dalamud.Plugin.Services;
using OtterGui.Classes;
+using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
-using Penumbra.Interop.ResourceLoading;
+using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
@@ -17,7 +18,7 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
-public class CollectionCacheManager : IDisposable
+public class CollectionCacheManager : IDisposable, IService
{
private readonly FrameworkManager _framework;
private readonly CommunicatorService _communicator;
@@ -70,7 +71,7 @@ public class CollectionCacheManager : IDisposable
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager);
if (!MetaFileManager.CharacterUtility.Ready)
- MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters;
+ MetaFileManager.CharacterUtility.LoadingFinished.Subscribe(IncrementCounters, CharacterUtilityFinished.Priority.CollectionCacheManager);
}
public void Dispose()
@@ -82,7 +83,13 @@ public class CollectionCacheManager : IDisposable
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange);
- MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
+ MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
+
+ foreach (var collection in _storage)
+ {
+ collection._cache?.Dispose();
+ collection._cache = null;
+ }
}
public void AddChange(CollectionCache.ChangeData data)
@@ -107,16 +114,16 @@ public class CollectionCacheManager : IDisposable
/// Only creates a new cache, does not update an existing one.
public bool CreateCache(ModCollection collection)
{
- if (collection.Index == ModCollection.Empty.Index)
+ if (collection.Identity.Index == ModCollection.Empty.Identity.Index)
return false;
if (collection._cache != null)
return false;
collection._cache = new CollectionCache(this, collection);
- if (collection.Index > 0)
+ if (collection.Identity.Index > 0)
Interlocked.Increment(ref _count);
- Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}.");
+ Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}.");
return true;
}
@@ -125,32 +132,32 @@ public class CollectionCacheManager : IDisposable
/// Does not create caches.
///
public void CalculateEffectiveFileList(ModCollection collection)
- => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier,
+ => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identity.Identifier,
() => CalculateEffectiveFileListInternal(collection));
private void CalculateEffectiveFileListInternal(ModCollection collection)
{
// Skip the empty collection.
- if (collection.Index == 0)
+ if (collection.Identity.Index == 0)
return;
- Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}");
+ Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName}");
if (!collection.HasCache)
{
Penumbra.Log.Error(
- $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists.");
+ $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists.");
}
else if (collection._cache!.Calculating != -1)
{
Penumbra.Log.Error(
- $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
+ $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
}
else
{
FullRecalculation(collection);
Penumbra.Log.Debug(
- $"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished.");
+ $"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.Identity.AnonymizedName} finished.");
}
}
@@ -164,8 +171,7 @@ public class CollectionCacheManager : IDisposable
try
{
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty,
- FullPath.Empty,
- null);
+ FullPath.Empty, null);
cache.ResolvedFiles.Clear();
cache.Meta.Reset();
cache.ConflictDict.Clear();
@@ -180,9 +186,7 @@ public class CollectionCacheManager : IDisposable
foreach (var mod in _modStorage)
cache.AddModSync(mod, false);
- cache.AddMetaFiles(true);
-
- collection.IncrementCounter();
+ collection.Counters.IncrementChange();
MetaFileManager.ApplyDefaultFiles(collection);
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty,
@@ -208,7 +212,7 @@ public class CollectionCacheManager : IDisposable
else
{
RemoveCache(old);
- if (type is not CollectionType.Inactive && newCollection != null && newCollection.Index != 0 && CreateCache(newCollection))
+ if (type is not CollectionType.Inactive && newCollection != null && newCollection.Identity.Index != 0 && CreateCache(newCollection))
CalculateEffectiveFileList(newCollection);
if (type is CollectionType.Default)
@@ -226,11 +230,11 @@ public class CollectionCacheManager : IDisposable
{
case ModPathChangeType.Deleted:
case ModPathChangeType.StartingReload:
- foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
+ foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
collection._cache!.RemoveMod(mod, true);
break;
case ModPathChangeType.Moved:
- foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
+ foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
collection._cache!.ReloadMod(mod, true);
break;
}
@@ -241,7 +245,7 @@ public class CollectionCacheManager : IDisposable
if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded))
return;
- foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
+ foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
collection._cache!.AddMod(mod, true);
}
@@ -253,12 +257,12 @@ public class CollectionCacheManager : IDisposable
private void RemoveCache(ModCollection? collection)
{
if (collection != null
- && collection.Index > ModCollection.Empty.Index
- && collection.Index != _active.Default.Index
- && collection.Index != _active.Interface.Index
- && collection.Index != _active.Current.Index
- && _active.SpecialAssignments.All(c => c.Value.Index != collection.Index)
- && _active.Individuals.All(c => c.Collection.Index != collection.Index))
+ && collection.Identity.Index > ModCollection.Empty.Identity.Index
+ && collection.Identity.Index != _active.Default.Identity.Index
+ && collection.Identity.Index != _active.Interface.Identity.Index
+ && collection.Identity.Index != _active.Current.Identity.Index
+ && _active.SpecialAssignments.All(c => c.Value.Identity.Index != collection.Identity.Index)
+ && _active.Individuals.All(c => c.Collection.Identity.Index != collection.Identity.Index))
ClearCache(collection);
}
@@ -268,7 +272,7 @@ public class CollectionCacheManager : IDisposable
{
if (type is ModOptionChangeType.PrepareChange)
{
- foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true }))
+ foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
collection._cache!.RemoveMod(mod, false);
return;
@@ -279,7 +283,7 @@ public class CollectionCacheManager : IDisposable
if (!recomputeList)
return;
- foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true }))
+ foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
{
if (justAdd)
collection._cache!.AddMod(mod, true);
@@ -292,8 +296,8 @@ public class CollectionCacheManager : IDisposable
private void IncrementCounters()
{
foreach (var collection in _storage.Where(c => c.HasCache))
- collection.IncrementCounter();
- MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
+ collection.Counters.IncrementChange();
+ MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _)
@@ -312,7 +316,7 @@ public class CollectionCacheManager : IDisposable
cache.AddMod(mod!, true);
else if (oldValue == Setting.True)
cache.RemoveMod(mod!, true);
- else if (collection[mod!.Index].Settings?.Enabled == true)
+ else if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
cache.ReloadMod(mod!, true);
else
cache.RemoveMod(mod!, true);
@@ -324,10 +328,13 @@ public class CollectionCacheManager : IDisposable
break;
case ModSettingChange.Setting:
- if (collection[mod!.Index].Settings?.Enabled == true)
- cache.ReloadMod(mod!, true);
+ if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
+ cache.ReloadMod(mod, true);
break;
+ case ModSettingChange.TemporarySetting:
+ cache.ReloadMod(mod!, true);
+ break;
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
FullRecalculation(collection);
@@ -354,9 +361,9 @@ public class CollectionCacheManager : IDisposable
collection._cache!.Dispose();
collection._cache = null;
- if (collection.Index > 0)
+ if (collection.Identity.Index > 0)
Interlocked.Decrement(ref _count);
- Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}.");
+ Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}.");
}
///
diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs
index d0a3bc76..295191d2 100644
--- a/Penumbra/Collections/Cache/CollectionModData.cs
+++ b/Penumbra/Collections/Cache/CollectionModData.cs
@@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache;
///
public class CollectionModData
{
- private readonly Dictionary, HashSet)> _data = new();
+ private readonly Dictionary, HashSet)> _data = new();
- public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data
- => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2));
+ public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data
+ => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2));
- public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod)
+ public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod)
{
if (_data.Remove(mod, out var data))
return data;
@@ -35,7 +35,7 @@ public class CollectionModData
}
}
- public void AddManip(IMod mod, MetaManipulation manipulation)
+ public void AddManip(IMod mod, IMetaIdentifier manipulation)
{
if (_data.TryGetValue(mod, out var data))
{
@@ -54,7 +54,7 @@ public class CollectionModData
_data.Remove(mod);
}
- public void RemoveManip(IMod mod, MetaManipulation manip)
+ public void RemoveManip(IMod mod, IMetaIdentifier manip)
{
if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
_data.Remove(mod);
diff --git a/Penumbra/Collections/Cache/CustomResourceCache.cs b/Penumbra/Collections/Cache/CustomResourceCache.cs
index 46c28393..e63f8637 100644
--- a/Penumbra/Collections/Cache/CustomResourceCache.cs
+++ b/Penumbra/Collections/Cache/CustomResourceCache.cs
@@ -1,6 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
-using Penumbra.Interop.ResourceLoading;
+using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.SafeHandles;
using Penumbra.String.Classes;
diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs
index a0f27c23..5e0626cf 100644
--- a/Penumbra/Collections/Cache/EqdpCache.cs
+++ b/Penumbra/Collections/Cache/EqdpCache.cs
@@ -1,97 +1,54 @@
-using OtterGui;
-using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
-using Penumbra.Interop.Services;
-using Penumbra.Interop.Structs;
using Penumbra.Meta;
-using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
-public readonly struct EqdpCache : IDisposable
+public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection)
{
- private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
- private readonly List _eqdpManipulations = new();
+ private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries =
+ [];
- public EqdpCache()
- { }
-
- public void SetFiles(MetaFileManager manager)
- {
- for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i)
- manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]);
- }
-
- public void SetFile(MetaFileManager manager, MetaIndex index)
- {
- var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
- if (i != -1)
- manager.SetFile(_eqdpFiles[i], index);
- }
-
- public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory)
- {
- var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
- if (idx < 0)
- {
- Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
- return null;
- }
-
- var i = CharacterUtilityData.EqdpIndices.IndexOf(idx);
- if (i < 0)
- {
- Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
- return null;
- }
-
- return manager.TemporarilySetFile(_eqdpFiles[i], idx);
- }
+ public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry)
+ => _fullEntries.TryGetValue((id, genderRace, accessory), out var pair)
+ ? (originalEntry & pair.InverseMask) | pair.Entry
+ : originalEntry;
public void Reset()
{
- foreach (var file in _eqdpFiles.OfType())
- {
- var relevant = CharacterUtility.RelevantIndices[file.Index.Value];
- file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId));
- }
-
- _eqdpManipulations.Clear();
+ Clear();
+ _fullEntries.Clear();
}
- public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
+ protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry)
{
- _eqdpManipulations.AddOrReplace(manip);
- var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??=
- new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar
- return manip.Apply(file);
+ var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
+ var mask = Eqdp.Mask(identifier.Slot);
+ var inverseMask = ~mask;
+ if (_fullEntries.TryGetValue(tuple, out var pair))
+ pair = ((pair.Entry & inverseMask) | (entry & mask), pair.InverseMask & inverseMask);
+ else
+ pair = (entry & mask, inverseMask);
+ _fullEntries[tuple] = pair;
}
- public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
+ protected override void RevertModInternal(EqdpIdentifier identifier)
{
- if (!_eqdpManipulations.Remove(manip))
- return false;
+ var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
- var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId);
- var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!;
- manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
- return manip.Apply(file);
+ if (!_fullEntries.Remove(tuple, out var pair))
+ return;
+
+ var mask = Eqdp.Mask(identifier.Slot);
+ var newMask = pair.InverseMask | mask;
+ if (newMask is not EqdpEntry.FullMask)
+ _fullEntries[tuple] = (pair.Entry & ~mask, newMask);
}
- public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory)
- => _eqdpFiles
- [Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
-
- public void Dispose()
+ protected override void Dispose(bool _)
{
- for (var i = 0; i < _eqdpFiles.Length; ++i)
- {
- _eqdpFiles[i]?.Dispose();
- _eqdpFiles[i] = null;
- }
-
- _eqdpManipulations.Clear();
+ Clear();
+ _fullEntries.Clear();
}
}
diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs
index 972ee5a5..da1a1d44 100644
--- a/Penumbra/Collections/Cache/EqpCache.cs
+++ b/Penumbra/Collections/Cache/EqpCache.cs
@@ -1,60 +1,66 @@
-using OtterGui.Filesystem;
-using Penumbra.Interop.Services;
-using Penumbra.Interop.Structs;
+using Penumbra.GameData.Enums;
+using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
-public struct EqpCache : IDisposable
+public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection)
{
- private ExpandedEqpFile? _eqpFile = null;
- private readonly List _eqpManipulations = new();
+ public unsafe EqpEntry GetValues(CharacterArmor* armor)
+ {
+ var bodyEntry = GetSingleValue(armor[1].Set, EquipSlot.Body);
+ var headEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHead)
+ ? GetSingleValue(armor[0].Set, EquipSlot.Head)
+ : GetSingleValue(armor[1].Set, EquipSlot.Head);
+ var handEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHand)
+ ? GetSingleValue(armor[2].Set, EquipSlot.Hands)
+ : GetSingleValue(armor[1].Set, EquipSlot.Hands);
+ var (legsEntry, legsId) = bodyEntry.HasFlag(EqpEntry.BodyShowLeg)
+ ? (GetSingleValue(armor[3].Set, EquipSlot.Legs), 3)
+ : (GetSingleValue(armor[1].Set, EquipSlot.Legs), 1);
+ var footEntry = legsEntry.HasFlag(EqpEntry.LegsShowFoot)
+ ? GetSingleValue(armor[4].Set, EquipSlot.Feet)
+ : GetSingleValue(armor[legsId].Set, EquipSlot.Feet);
- public EqpCache()
- { }
+ var combined = bodyEntry | headEntry | handEntry | legsEntry | footEntry;
+ return PostProcessFeet(PostProcessHands(combined));
+ }
- public void SetFiles(MetaFileManager manager)
- => manager.SetFile(_eqpFile, MetaIndex.Eqp);
-
- public static void ResetFiles(MetaFileManager manager)
- => manager.SetFile(null, MetaIndex.Eqp);
-
- public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
- => manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp);
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot)
+ => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot);
public void Reset()
- {
- if (_eqpFile == null)
- return;
+ => Clear();
- _eqpFile.Reset(_eqpManipulations.Select(m => m.SetId));
- _eqpManipulations.Clear();
+ protected override void Dispose(bool _)
+ => Clear();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private static EqpEntry PostProcessHands(EqpEntry entry)
+ {
+ if (!entry.HasFlag(EqpEntry.HandsHideForearm))
+ return entry;
+
+ var testFlag = entry.HasFlag(EqpEntry.HandsHideElbow)
+ ? entry.HasFlag(EqpEntry.BodyHideGlovesL)
+ : entry.HasFlag(EqpEntry.BodyHideGlovesM);
+ return testFlag
+ ? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS
+ : entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS);
}
- public bool ApplyMod(MetaFileManager manager, EqpManipulation manip)
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private static EqpEntry PostProcessFeet(EqpEntry entry)
{
- _eqpManipulations.AddOrReplace(manip);
- _eqpFile ??= new ExpandedEqpFile(manager);
- return manip.Apply(_eqpFile);
- }
+ if (!entry.HasFlag(EqpEntry.FeetHideCalf))
+ return entry;
- public bool RevertMod(MetaFileManager manager, EqpManipulation manip)
- {
- var idx = _eqpManipulations.FindIndex(manip.Equals);
- if (idx < 0)
- return false;
+ if (entry.HasFlag(EqpEntry.FeetHideKnee) || !entry.HasFlag(EqpEntry._20))
+ return entry & ~(EqpEntry.LegsHideBootsS | EqpEntry.LegsHideBootsM);
- var def = ExpandedEqpFile.GetDefault(manager, manip.SetId);
- manip = new EqpManipulation(def, manip.Slot, manip.SetId);
- return manip.Apply(_eqpFile!);
- }
-
- public void Dispose()
- {
- _eqpFile?.Dispose();
- _eqpFile = null;
- _eqpManipulations.Clear();
+ return (entry | EqpEntry.LegsHideBootsM) & ~EqpEntry.LegsHideBootsS;
}
}
diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs
index 2552cd4a..aff8beef 100644
--- a/Penumbra/Collections/Cache/EstCache.cs
+++ b/Penumbra/Collections/Cache/EstCache.cs
@@ -1,138 +1,19 @@
-using OtterGui.Filesystem;
-using Penumbra.GameData.Enums;
-using Penumbra.GameData.Structs;
-using Penumbra.Interop.Services;
-using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
-public struct EstCache : IDisposable
+public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection)
{
- private EstFile? _estFaceFile = null;
- private EstFile? _estHairFile = null;
- private EstFile? _estBodyFile = null;
- private EstFile? _estHeadFile = null;
-
- private readonly List _estManipulations = new();
-
- public EstCache()
- { }
-
- public void SetFiles(MetaFileManager manager)
- {
- manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
- manager.SetFile(_estHairFile, MetaIndex.HairEst);
- manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
- manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
- }
-
- public void SetFile(MetaFileManager manager, MetaIndex index)
- {
- switch (index)
- {
- case MetaIndex.FaceEst:
- manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
- break;
- case MetaIndex.HairEst:
- manager.SetFile(_estHairFile, MetaIndex.HairEst);
- break;
- case MetaIndex.BodyEst:
- manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
- break;
- case MetaIndex.HeadEst:
- manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
- break;
- }
- }
-
- public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type)
- {
- var (file, idx) = type switch
- {
- EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst),
- EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst),
- EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
- EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst),
- _ => (null, 0),
- };
-
- return manager.TemporarilySetFile(file, idx);
- }
-
- private readonly EstFile? GetEstFile(EstManipulation.EstType type)
- {
- return type switch
- {
- EstManipulation.EstType.Face => _estFaceFile,
- EstManipulation.EstType.Hair => _estHairFile,
- EstManipulation.EstType.Body => _estBodyFile,
- EstManipulation.EstType.Head => _estHeadFile,
- _ => null,
- };
- }
-
- internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId)
- {
- var file = GetEstFile(type);
- return file != null
- ? file[genderRace, primaryId.Id]
- : EstFile.GetDefault(manager, type, genderRace, primaryId);
- }
+ public EstEntry GetEstEntry(EstIdentifier identifier)
+ => TryGetValue(identifier, out var entry)
+ ? entry.Entry
+ : EstFile.GetDefault(Manager, identifier);
public void Reset()
- {
- _estFaceFile?.Reset();
- _estHairFile?.Reset();
- _estBodyFile?.Reset();
- _estHeadFile?.Reset();
- _estManipulations.Clear();
- }
+ => Clear();
- public bool ApplyMod(MetaFileManager manager, EstManipulation m)
- {
- _estManipulations.AddOrReplace(m);
- var file = m.Slot switch
- {
- EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair),
- EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face),
- EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body),
- EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head),
- _ => throw new ArgumentOutOfRangeException(),
- };
- return m.Apply(file);
- }
-
- public bool RevertMod(MetaFileManager manager, EstManipulation m)
- {
- if (!_estManipulations.Remove(m))
- return false;
-
- var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId);
- var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def);
- var file = m.Slot switch
- {
- EstManipulation.EstType.Hair => _estHairFile!,
- EstManipulation.EstType.Face => _estFaceFile!,
- EstManipulation.EstType.Body => _estBodyFile!,
- EstManipulation.EstType.Head => _estHeadFile!,
- _ => throw new ArgumentOutOfRangeException(),
- };
- return manip.Apply(file);
- }
-
- public void Dispose()
- {
- _estFaceFile?.Dispose();
- _estHairFile?.Dispose();
- _estBodyFile?.Dispose();
- _estHeadFile?.Dispose();
- _estFaceFile = null;
- _estHairFile = null;
- _estBodyFile = null;
- _estHeadFile = null;
- _estManipulations.Clear();
- }
+ protected override void Dispose(bool _)
+ => Clear();
}
diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs
similarity index 53%
rename from Penumbra/Meta/Manipulations/GlobalEqpCache.cs
rename to Penumbra/Collections/Cache/GlobalEqpCache.cs
index 48ffb308..7d2fbf64 100644
--- a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs
+++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs
@@ -1,9 +1,12 @@
+using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Structs;
+using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Editor;
-namespace Penumbra.Meta.Manipulations;
+namespace Penumbra.Collections.Cache;
-public struct GlobalEqpCache : IService
+public class GlobalEqpCache : ReadWriteDictionary, IService
{
private readonly HashSet _doNotHideEarrings = [];
private readonly HashSet _doNotHideNecklace = [];
@@ -12,12 +15,13 @@ public struct GlobalEqpCache : IService
private readonly HashSet _doNotHideRingR = [];
private bool _doNotHideVieraHats;
private bool _doNotHideHrothgarHats;
+ private bool _hideAuRaHorns;
+ private bool _hideVieraEars;
+ private bool _hideMiqoteEars;
- public GlobalEqpCache()
- { }
-
- public void Clear()
+ public new void Clear()
{
+ base.Clear();
_doNotHideEarrings.Clear();
_doNotHideNecklace.Clear();
_doNotHideBracelets.Clear();
@@ -25,18 +29,36 @@ public struct GlobalEqpCache : IService
_doNotHideRingR.Clear();
_doNotHideHrothgarHats = false;
_doNotHideVieraHats = false;
+ _hideAuRaHorns = false;
+ _hideVieraEars = false;
+ _hideMiqoteEars = false;
}
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
{
+ if (Count == 0)
+ return original;
+
if (_doNotHideVieraHats)
original |= EqpEntry.HeadShowVieraHat;
if (_doNotHideHrothgarHats)
original |= EqpEntry.HeadShowHrothgarHat;
+ if (_hideAuRaHorns)
+ original &= ~EqpEntry.HeadShowEarAuRa;
+
+ if (_hideVieraEars)
+ original &= ~EqpEntry.HeadShowEarViera;
+
+ if (_hideMiqoteEars)
+ original &= ~EqpEntry.HeadShowEarMiqote;
+
if (_doNotHideEarrings.Contains(armor[5].Set))
- original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman;
+ original |= EqpEntry.HeadShowEarringsHyurRoe
+ | EqpEntry.HeadShowEarringsLalaElezen
+ | EqpEntry.HeadShowEarringsMiqoHrothViera
+ | EqpEntry.HeadShowEarringsAura;
if (_doNotHideNecklace.Contains(armor[6].Set))
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
@@ -44,16 +66,22 @@ public struct GlobalEqpCache : IService
if (_doNotHideBracelets.Contains(armor[7].Set))
original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet;
- if (_doNotHideBracelets.Contains(armor[8].Set))
+ if (_doNotHideRingR.Contains(armor[8].Set))
original |= EqpEntry.HandShowRingR;
- if (_doNotHideBracelets.Contains(armor[9].Set))
+ if (_doNotHideRingL.Contains(armor[9].Set))
original |= EqpEntry.HandShowRingL;
+
return original;
}
- public bool Add(GlobalEqpManipulation manipulation)
- => manipulation.Type switch
+ public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation)
+ {
+ if (Remove(manipulation, out var oldMod) && oldMod == mod)
+ return false;
+
+ this[manipulation] = mod;
+ _ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition),
@@ -61,12 +89,21 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
- GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
- _ => false,
+ GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
+ GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true),
+ GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true),
+ GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true),
+ _ => false,
};
+ return true;
+ }
- public bool Remove(GlobalEqpManipulation manipulation)
- => manipulation.Type switch
+ public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod)
+ {
+ if (!Remove(manipulation, out mod))
+ return false;
+
+ _ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition),
@@ -74,7 +111,12 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
- GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
- _ => false,
+ GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
+ GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false),
+ GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false),
+ GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false),
+ _ => false,
};
+ return true;
+ }
}
diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs
index 0a713867..9170b871 100644
--- a/Penumbra/Collections/Cache/GmpCache.cs
+++ b/Penumbra/Collections/Cache/GmpCache.cs
@@ -1,56 +1,14 @@
-using OtterGui.Filesystem;
-using Penumbra.Interop.Services;
-using Penumbra.Interop.Structs;
+using Penumbra.GameData.Structs;
using Penumbra.Meta;
-using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
-public struct GmpCache : IDisposable
+public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection)
{
- private ExpandedGmpFile? _gmpFile = null;
- private readonly List _gmpManipulations = new();
-
- public GmpCache()
- { }
-
- public void SetFiles(MetaFileManager manager)
- => manager.SetFile(_gmpFile, MetaIndex.Gmp);
-
- public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
- => manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp);
-
public void Reset()
- {
- if (_gmpFile == null)
- return;
+ => Clear();
- _gmpFile.Reset(_gmpManipulations.Select(m => m.SetId));
- _gmpManipulations.Clear();
- }
-
- public bool ApplyMod(MetaFileManager manager, GmpManipulation manip)
- {
- _gmpManipulations.AddOrReplace(manip);
- _gmpFile ??= new ExpandedGmpFile(manager);
- return manip.Apply(_gmpFile);
- }
-
- public bool RevertMod(MetaFileManager manager, GmpManipulation manip)
- {
- if (!_gmpManipulations.Remove(manip))
- return false;
-
- var def = ExpandedGmpFile.GetDefault(manager, manip.SetId);
- manip = new GmpManipulation(def, manip.SetId);
- return manip.Apply(_gmpFile!);
- }
-
- public void Dispose()
- {
- _gmpFile?.Dispose();
- _gmpFile = null;
- _gmpManipulations.Clear();
- }
+ protected override void Dispose(bool _)
+ => Clear();
}
diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs
index 843fe195..461ffccc 100644
--- a/Penumbra/Collections/Cache/ImcCache.cs
+++ b/Penumbra/Collections/Cache/ImcCache.cs
@@ -1,123 +1,102 @@
+using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
-using Penumbra.String.Classes;
+using Penumbra.String;
namespace Penumbra.Collections.Cache;
-public readonly struct ImcCache : IDisposable
+public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection)
{
- private readonly Dictionary _imcFiles = new();
- private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new();
+ private readonly Dictionary)> _imcFiles = [];
- public ImcCache()
- { }
+ public bool HasFile(CiByteString path)
+ => _imcFiles.ContainsKey(path);
- public void SetFiles(ModCollection collection, bool fromFullCompute)
+ public bool GetFile(CiByteString path, [NotNullWhen(true)] out ImcFile? file)
{
- if (fromFullCompute)
- foreach (var path in _imcFiles.Keys)
- collection._cache!.ForceFileSync(path, CreateImcPath(collection, path));
- else
- foreach (var path in _imcFiles.Keys)
- collection._cache!.ForceFile(path, CreateImcPath(collection, path));
- }
-
- public void Reset(ModCollection collection)
- {
- foreach (var (path, file) in _imcFiles)
+ if (!_imcFiles.TryGetValue(path, out var p))
{
- collection._cache!.RemovePath(path);
- file.Reset();
- }
-
- _imcManipulations.Clear();
- }
-
- public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip)
- {
- if (!manip.Validate(true))
+ file = null;
return false;
-
- var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip));
- if (idx < 0)
- {
- idx = _imcManipulations.Count;
- _imcManipulations.Add((manip, null!));
}
- var path = manip.GamePath();
+ file = p.Item1;
+ return true;
+ }
+
+ public void Reset()
+ {
+ foreach (var (_, (file, set)) in _imcFiles)
+ {
+ file.Reset();
+ set.Clear();
+ }
+
+ _imcFiles.Clear();
+ Clear();
+ }
+
+ protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry)
+ {
+ Collection.Counters.IncrementImc();
+ ApplyFile(identifier, entry);
+ }
+
+ private void ApplyFile(ImcIdentifier identifier, ImcEntry entry)
+ {
+ var path = identifier.GamePath().Path;
try
{
- if (!_imcFiles.TryGetValue(path, out var file))
- file = new ImcFile(manager, manip);
+ if (!_imcFiles.TryGetValue(path, out var pair))
+ pair = (new ImcFile(Manager, identifier), []);
- _imcManipulations[idx] = (manip, file);
- if (!manip.Apply(file))
- return false;
+ if (!Apply(pair.Item1, identifier, entry))
+ return;
- _imcFiles[path] = file;
- var fullPath = CreateImcPath(collection, path);
- collection._cache!.ForceFile(path, fullPath);
-
- return true;
+ pair.Item2.Add(identifier);
+ _imcFiles[path] = pair;
}
catch (ImcException e)
{
- manager.ValidityChecker.ImcExceptions.Add(e);
+ Manager.ValidityChecker.ImcExceptions.Add(e);
Penumbra.Log.Error(e.ToString());
}
catch (Exception e)
{
- Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}");
+ Penumbra.Log.Error($"Could not apply IMC Manipulation {identifier}:\n{e}");
}
-
- return false;
}
- public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m)
+ protected override void RevertModInternal(ImcIdentifier identifier)
{
- if (!m.Validate(false))
- return false;
+ Collection.Counters.IncrementImc();
+ var path = identifier.GamePath().Path;
+ if (!_imcFiles.TryGetValue(path, out var pair))
+ return;
- var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m));
- if (idx < 0)
- return false;
+ if (!pair.Item2.Remove(identifier))
+ return;
- var (_, file) = _imcManipulations[idx];
- _imcManipulations.RemoveAt(idx);
-
- if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file)))
+ if (pair.Item2.Count == 0)
{
- _imcFiles.Remove(file.Path);
- collection._cache!.ForceFile(file.Path, FullPath.Empty);
- file.Dispose();
- return true;
+ _imcFiles.Remove(path);
+ pair.Item1.Dispose();
+ return;
}
- var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _);
- var manip = m.Copy(def);
- if (!manip.Apply(file))
- return false;
-
- var fullPath = CreateImcPath(collection, file.Path);
- collection._cache!.ForceFile(file.Path, fullPath);
-
- return true;
+ var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _);
+ Apply(pair.Item1, identifier, def);
}
- public void Dispose()
+ public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry)
+ => file.SetEntry(ImcFile.PartIndex(identifier.EquipSlot), identifier.Variant.Id, entry);
+
+ protected override void Dispose(bool _)
{
- foreach (var file in _imcFiles.Values)
+ foreach (var (_, (file, _)) in _imcFiles)
file.Dispose();
-
+ Clear();
_imcFiles.Clear();
- _imcManipulations.Clear();
}
-
- private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path)
- => new($"|{collection.Id.OptimizedString()}_{collection.ChangeCounter}|{path}");
-
- public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
- => _imcFiles.TryGetValue(path, out file);
}
diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs
index f42b72fc..011cdd23 100644
--- a/Penumbra/Collections/Cache/MetaCache.cs
+++ b/Penumbra/Collections/Cache/MetaCache.cs
@@ -1,246 +1,137 @@
using Penumbra.GameData.Enums;
+using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Structs;
-using Penumbra.Interop.Services;
-using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
-using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
-public class MetaCache : IDisposable, IEnumerable>
+public class MetaCache(MetaFileManager manager, ModCollection collection)
{
- private readonly MetaFileManager _manager;
- private readonly ModCollection _collection;
- private readonly Dictionary _manipulations = new();
- private EqpCache _eqpCache = new();
- private readonly EqdpCache _eqdpCache = new();
- private EstCache _estCache = new();
- private GmpCache _gmpCache = new();
- private CmpCache _cmpCache = new();
- private readonly ImcCache _imcCache = new();
- private GlobalEqpCache _globalEqpCache = new();
-
- public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
- {
- lock (_manipulations)
- {
- return _manipulations.TryGetValue(manip, out mod);
- }
- }
+ public readonly EqpCache Eqp = new(manager, collection);
+ public readonly EqdpCache Eqdp = new(manager, collection);
+ public readonly EstCache Est = new(manager, collection);
+ public readonly GmpCache Gmp = new(manager, collection);
+ public readonly RspCache Rsp = new(manager, collection);
+ public readonly ImcCache Imc = new(manager, collection);
+ public readonly AtchCache Atch = new(manager, collection);
+ public readonly ShpCache Shp = new(manager, collection);
+ public readonly AtrCache Atr = new(manager, collection);
+ public readonly GlobalEqpCache GlobalEqp = new();
+ public bool IsDisposed { get; private set; }
public int Count
- => _manipulations.Count;
+ => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
- public IReadOnlyCollection Manipulations
- => _manipulations.Keys;
-
- public IEnumerator> GetEnumerator()
- => _manipulations.GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator()
- => GetEnumerator();
-
- public MetaCache(MetaFileManager manager, ModCollection collection)
- {
- _manager = manager;
- _collection = collection;
- if (!_manager.CharacterUtility.Ready)
- _manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
- }
-
- public void SetFiles()
- {
- _eqpCache.SetFiles(_manager);
- _eqdpCache.SetFiles(_manager);
- _estCache.SetFiles(_manager);
- _gmpCache.SetFiles(_manager);
- _cmpCache.SetFiles(_manager);
- _imcCache.SetFiles(_collection, false);
- }
+ public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
+ => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
+ .Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
+ .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void Reset()
{
- _eqpCache.Reset();
- _eqdpCache.Reset();
- _estCache.Reset();
- _gmpCache.Reset();
- _cmpCache.Reset();
- _imcCache.Reset(_collection);
- _manipulations.Clear();
- _globalEqpCache.Clear();
+ Eqp.Reset();
+ Eqdp.Reset();
+ Est.Reset();
+ Gmp.Reset();
+ Rsp.Reset();
+ Imc.Reset();
+ Atch.Reset();
+ Shp.Reset();
+ Atr.Reset();
+ GlobalEqp.Clear();
}
public void Dispose()
{
- _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
- _eqpCache.Dispose();
- _eqdpCache.Dispose();
- _estCache.Dispose();
- _gmpCache.Dispose();
- _cmpCache.Dispose();
- _imcCache.Dispose();
- _manipulations.Clear();
+ if (IsDisposed)
+ return;
+
+ IsDisposed = true;
+ Eqp.Dispose();
+ Eqdp.Dispose();
+ Est.Dispose();
+ Gmp.Dispose();
+ Rsp.Dispose();
+ Imc.Dispose();
+ Atch.Dispose();
+ Shp.Dispose();
+ Atr.Dispose();
}
+ public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
+ {
+ mod = null;
+ return identifier switch
+ {
+ EqdpIdentifier i => Eqdp.TryGetValue(i, out var p) && Convert(p, out mod),
+ EqpIdentifier i => Eqp.TryGetValue(i, out var p) && Convert(p, out mod),
+ EstIdentifier i => Est.TryGetValue(i, out var p) && Convert(p, out mod),
+ GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod),
+ ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
+ RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
+ AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
+ ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod),
+ AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod),
+ GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
+ _ => false,
+ };
+
+ static bool Convert((IMod, T) pair, out IMod mod)
+ {
+ mod = pair.Item1;
+ return true;
+ }
+ }
+
+ public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
+ => identifier switch
+ {
+ EqdpIdentifier i => Eqdp.RevertMod(i, out mod),
+ EqpIdentifier i => Eqp.RevertMod(i, out mod),
+ EstIdentifier i => Est.RevertMod(i, out mod),
+ GmpIdentifier i => Gmp.RevertMod(i, out mod),
+ ImcIdentifier i => Imc.RevertMod(i, out mod),
+ RspIdentifier i => Rsp.RevertMod(i, out mod),
+ AtchIdentifier i => Atch.RevertMod(i, out mod),
+ ShpIdentifier i => Shp.RevertMod(i, out mod),
+ AtrIdentifier i => Atr.RevertMod(i, out mod),
+ GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
+ _ => (mod = null) != null,
+ };
+
+ public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry)
+ => identifier switch
+ {
+ EqdpIdentifier i when entry is EqdpEntry e => Eqdp.ApplyMod(mod, i, e),
+ EqdpIdentifier i when entry is EqdpEntryInternal e => Eqdp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
+ EqpIdentifier i when entry is EqpEntry e => Eqp.ApplyMod(mod, i, e),
+ EqpIdentifier i when entry is EqpEntryInternal e => Eqp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
+ EstIdentifier i when entry is EstEntry e => Est.ApplyMod(mod, i, e),
+ GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e),
+ ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
+ RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
+ AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
+ ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e),
+ AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e),
+ GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
+ _ => false,
+ };
+
~MetaCache()
=> Dispose();
- public bool ApplyMod(MetaManipulation manip, IMod mod)
- {
- lock (_manipulations)
- {
- if (_manipulations.ContainsKey(manip))
- _manipulations.Remove(manip);
-
- _manipulations[manip] = mod;
- }
-
- if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp)
- return _globalEqpCache.Add(manip.GlobalEqp);
-
- if (!_manager.CharacterUtility.Ready)
- return true;
-
- // Imc manipulations do not require character utility,
- // but they do require the file space to be ready.
- return manip.ManipulationType switch
- {
- MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
- MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
- MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
- MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
- MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
- MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
- MetaManipulation.Type.Unknown => false,
- _ => false,
- };
- }
-
- public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
- {
- lock (_manipulations)
- {
- var ret = _manipulations.Remove(manip, out mod);
-
- if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp)
- return _globalEqpCache.Remove(manip.GlobalEqp);
-
- if (!_manager.CharacterUtility.Ready)
- return ret;
- }
-
- // Imc manipulations do not require character utility,
- // but they do require the file space to be ready.
- return manip.ManipulationType switch
- {
- MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp),
- MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp),
- MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est),
- MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp),
- MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp),
- MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc),
- MetaManipulation.Type.Unknown => false,
- _ => false,
- };
- }
-
- /// Set a single file.
- public void SetFile(MetaIndex metaIndex)
- {
- switch (metaIndex)
- {
- case MetaIndex.Eqp:
- _eqpCache.SetFiles(_manager);
- break;
- case MetaIndex.Gmp:
- _gmpCache.SetFiles(_manager);
- break;
- case MetaIndex.HumanCmp:
- _cmpCache.SetFiles(_manager);
- break;
- case MetaIndex.FaceEst:
- case MetaIndex.HairEst:
- case MetaIndex.HeadEst:
- case MetaIndex.BodyEst:
- _estCache.SetFile(_manager, metaIndex);
- break;
- default:
- _eqdpCache.SetFile(_manager, metaIndex);
- break;
- }
- }
-
- /// Set the currently relevant IMC files for the collection cache.
- public void SetImcFiles(bool fromFullCompute)
- => _imcCache.SetFiles(_collection, fromFullCompute);
-
- public MetaList.MetaReverter TemporarilySetEqpFile()
- => _eqpCache.TemporarilySetFiles(_manager);
-
- public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
- => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory);
-
- public MetaList.MetaReverter TemporarilySetGmpFile()
- => _gmpCache.TemporarilySetFiles(_manager);
-
- public MetaList.MetaReverter TemporarilySetCmpFile()
- => _cmpCache.TemporarilySetFiles(_manager);
-
- public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type)
- => _estCache.TemporarilySetFiles(_manager, type);
-
- public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor)
- => _globalEqpCache.Apply(baseEntry, armor);
-
-
- /// Try to obtain a manipulated IMC file.
- public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
- => _imcCache.GetImcFile(path, out file);
-
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId)
- {
- var eqdpFile = _eqdpCache.EqdpFile(race, accessory);
- if (eqdpFile != null)
- return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default;
+ => Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId));
- return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId);
- }
-
- internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId)
- => _estCache.GetEstEntry(_manager, type, genderRace, primaryId);
-
- /// Use this when CharacterUtility becomes ready.
- private void ApplyStoredManipulations()
- {
- if (!_manager.CharacterUtility.Ready)
- return;
-
- var loaded = 0;
- lock (_manipulations)
- {
- foreach (var manip in Manipulations)
- {
- loaded += manip.ManipulationType switch
- {
- MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
- MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
- MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
- MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
- MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
- MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
- MetaManipulation.Type.GlobalEqp => false,
- MetaManipulation.Type.Unknown => false,
- _ => false,
- }
- ? 1
- : 0;
- }
- }
-
- _manager.ApplyDefaultFiles(_collection);
- _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
- Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations.");
- }
+ internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
+ => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace));
}
diff --git a/Penumbra/Collections/Cache/MetaCacheBase.cs b/Penumbra/Collections/Cache/MetaCacheBase.cs
new file mode 100644
index 00000000..98a87e3f
--- /dev/null
+++ b/Penumbra/Collections/Cache/MetaCacheBase.cs
@@ -0,0 +1,47 @@
+using OtterGui.Classes;
+using Penumbra.Meta;
+using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Editor;
+
+namespace Penumbra.Collections.Cache;
+
+public abstract class MetaCacheBase(MetaFileManager manager, ModCollection collection)
+ : ReadWriteDictionary
+ where TIdentifier : unmanaged, IMetaIdentifier
+ where TEntry : unmanaged
+{
+ protected readonly MetaFileManager Manager = manager;
+ protected readonly ModCollection Collection = collection;
+
+ public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry)
+ {
+ if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.Default.Equals(pair.Entry, entry))
+ return false;
+
+ this[identifier] = (source, entry);
+
+ ApplyModInternal(identifier, entry);
+ return true;
+ }
+
+ public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
+ {
+ if (!Remove(identifier, out var pair))
+ {
+ mod = null;
+ return false;
+ }
+
+ mod = pair.Source;
+
+ RevertModInternal(identifier);
+ return true;
+ }
+
+
+ protected virtual void ApplyModInternal(TIdentifier identifier, TEntry entry)
+ { }
+
+ protected virtual void RevertModInternal(TIdentifier identifier)
+ { }
+}
diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs
new file mode 100644
index 00000000..064b1f44
--- /dev/null
+++ b/Penumbra/Collections/Cache/RspCache.cs
@@ -0,0 +1,13 @@
+using Penumbra.Meta;
+using Penumbra.Meta.Manipulations;
+
+namespace Penumbra.Collections.Cache;
+
+public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection)
+{
+ public void Reset()
+ => Clear();
+
+ protected override void Dispose(bool _)
+ => Clear();
+}
diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
new file mode 100644
index 00000000..4c61bdd2
--- /dev/null
+++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
@@ -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 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 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,
+ };
+ }
+}
diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs
new file mode 100644
index 00000000..d8c3a036
--- /dev/null
+++ b/Penumbra/Collections/Cache/ShpCache.cs
@@ -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(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 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 _shpData = [];
+ private readonly Dictionary _wristConnectors = [];
+ private readonly Dictionary _waistConnectors = [];
+ private readonly Dictionary _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 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 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);
+ }
+ }
+ }
+}
diff --git a/Penumbra/Collections/CollectionAutoSelector.cs b/Penumbra/Collections/CollectionAutoSelector.cs
new file mode 100644
index 00000000..f6e6bf72
--- /dev/null
+++ b/Penumbra/Collections/CollectionAutoSelector.cs
@@ -0,0 +1,82 @@
+using Dalamud.Plugin.Services;
+using OtterGui.Services;
+using Penumbra.Collections.Manager;
+using Penumbra.GameData.Interop;
+using Penumbra.Interop.PathResolving;
+
+namespace Penumbra.Collections;
+
+public sealed class CollectionAutoSelector : IService, IDisposable
+{
+ private readonly Configuration _config;
+ private readonly ActiveCollections _collections;
+ private readonly IClientState _clientState;
+ private readonly CollectionResolver _resolver;
+ private readonly ObjectManager _objects;
+
+ public CollectionAutoSelector(Configuration config, ActiveCollections collections, IClientState clientState, CollectionResolver resolver,
+ ObjectManager objects)
+ {
+ _config = config;
+ _collections = collections;
+ _clientState = clientState;
+ _resolver = resolver;
+ _objects = objects;
+
+ if (_config.AutoSelectCollection)
+ Attach();
+ }
+
+ public bool Disposed { get; private set; }
+
+ public void SetAutomaticSelection(bool value)
+ {
+ _config.AutoSelectCollection = value;
+ if (value)
+ Attach();
+ else
+ Detach();
+ }
+
+ private void Attach()
+ {
+ if (Disposed)
+ return;
+
+ _clientState.Login += OnLogin;
+ Select();
+ }
+
+ private void OnLogin()
+ => Select();
+
+ private void Detach()
+ => _clientState.Login -= OnLogin;
+
+ private void Select()
+ {
+ if (!_objects[0].IsCharacter)
+ return;
+
+ var collection = _resolver.PlayerCollection();
+ if (collection.Identity.Id == Guid.Empty)
+ {
+ Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
+ }
+ else
+ {
+ Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
+ _collections.SetCollection(collection, CollectionType.Current);
+ }
+ }
+
+
+ public void Dispose()
+ {
+ if (Disposed)
+ return;
+
+ Disposed = true;
+ Detach();
+ }
+}
diff --git a/Penumbra/Collections/CollectionCounters.cs b/Penumbra/Collections/CollectionCounters.cs
new file mode 100644
index 00000000..6ca0d0a0
--- /dev/null
+++ b/Penumbra/Collections/CollectionCounters.cs
@@ -0,0 +1,28 @@
+namespace Penumbra.Collections;
+
+public struct CollectionCounters(int changeCounter)
+{
+ /// Count the number of changes of the effective file list.
+ public int Change { get; private set; } = changeCounter;
+
+ /// Count the number of IMC-relevant changes of the effective file list.
+ public int Imc { get; private set; }
+
+ /// Count the number of ATCH-relevant changes of the effective file list.
+ public int Atch { get; private set; }
+
+ /// Increment the number of changes in the effective file list.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int IncrementChange()
+ => ++Change;
+
+ /// Increment the number of IMC-relevant changes in the effective file list.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int IncrementImc()
+ => ++Imc;
+
+ /// Increment the number of ATCH-relevant changes in the effective file list.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int IncrementAtch()
+ => ++Atch;
+}
diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs
index 2f9e9b15..b4af0998 100644
--- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs
+++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs
@@ -1,4 +1,4 @@
-using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
@@ -48,7 +48,7 @@ public static class ActiveCollectionMigration
if (!storage.ByName(collectionName, out var collection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning);
+ $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning);
dict.Add(player, ModCollection.Empty);
}
else
diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs
index 4e8ebe36..ffec7fd2 100644
--- a/Penumbra/Collections/Manager/ActiveCollections.cs
+++ b/Penumbra/Collections/Manager/ActiveCollections.cs
@@ -1,8 +1,9 @@
-using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
-using OtterGui;
using OtterGui.Classes;
+using OtterGui.Extensions;
+using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
@@ -11,7 +12,7 @@ using Penumbra.UI;
namespace Penumbra.Collections.Manager;
-public class ActiveCollectionData
+public class ActiveCollectionData : IService
{
public ModCollection Current { get; internal set; } = ModCollection.Empty;
public ModCollection Default { get; internal set; } = ModCollection.Empty;
@@ -20,7 +21,7 @@ public class ActiveCollectionData
public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues().Length - 3];
}
-public class ActiveCollections : ISavable, IDisposable
+public class ActiveCollections : ISavable, IDisposable, IService
{
public const int Version = 2;
@@ -218,7 +219,7 @@ public class ActiveCollections : ISavable, IDisposable
_ => null,
};
- if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count)
+ if (oldCollection == null || collection == oldCollection || collection.Identity.Index >= _storage.Count)
return;
switch (collectionType)
@@ -261,13 +262,13 @@ public class ActiveCollections : ISavable, IDisposable
var jObj = new JObject
{
{ nameof(Version), Version },
- { nameof(Default), Default.Id },
- { nameof(Interface), Interface.Id },
- { nameof(Current), Current.Id },
+ { nameof(Default), Default.Identity.Id },
+ { nameof(Interface), Interface.Identity.Id },
+ { nameof(Current), Current.Identity.Id },
};
foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!)))
- jObj.Add(type.ToString(), collection.Id);
+ jObj.Add(type.ToString(), collection.Identity.Id);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer);
@@ -281,7 +282,7 @@ public class ActiveCollections : ISavable, IDisposable
.Prepend(Interface)
.Prepend(Default)
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
- .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
+ .SelectMany(c => c.Inheritance.FlatHierarchy).Contains(Current);
/// Save if any of the active collections is changed and set new collections to Current.
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3)
@@ -299,7 +300,7 @@ public class ActiveCollections : ISavable, IDisposable
if (oldCollection == Interface)
SetCollection(ModCollection.Empty, CollectionType.Interface);
if (oldCollection == Current)
- SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
+ SetCollection(Default.Identity.Index > ModCollection.Empty.Identity.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
for (var i = 0; i < SpecialCollections.Length; ++i)
{
@@ -324,11 +325,11 @@ public class ActiveCollections : ISavable, IDisposable
{
var configChanged = false;
// Load the default collection. If the name does not exist take the empty collection.
- var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Name;
+ var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Identity.Name;
if (!_storage.ByName(defaultName, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.",
+ $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@@ -339,11 +340,11 @@ public class ActiveCollections : ISavable, IDisposable
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
- var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name;
+ var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Identity.Name;
if (!_storage.ByName(interfaceName, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
+ $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@@ -354,11 +355,11 @@ public class ActiveCollections : ISavable, IDisposable
}
// Load the current collection.
- var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Name;
+ var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Identity.Name;
if (!_storage.ByName(currentName, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.",
+ $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
@@ -403,7 +404,7 @@ public class ActiveCollections : ISavable, IDisposable
if (!_storage.ById(defaultId, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.",
+ $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@@ -414,11 +415,11 @@ public class ActiveCollections : ISavable, IDisposable
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
- var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Id;
+ var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Identity.Id;
if (!_storage.ById(interfaceId, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.",
+ $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@@ -429,11 +430,11 @@ public class ActiveCollections : ISavable, IDisposable
}
// Load the current collection.
- var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Id;
+ var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Identity.Id;
if (!_storage.ById(currentId, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.",
+ $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
@@ -586,7 +587,7 @@ public class ActiveCollections : ISavable, IDisposable
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
{
var global = ByType(CollectionType.Individual, _actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
- return global?.Index == checkAssignment.Index
+ return (global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
: string.Empty;
}
@@ -595,12 +596,12 @@ public class ActiveCollections : ISavable, IDisposable
{
var global = ByType(CollectionType.Individual,
_actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
- if (global?.Index == checkAssignment.Index)
+ if ((global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index)
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
}
var unowned = ByType(CollectionType.Individual, _actors.CreateNpc(id.Kind, id.DataId));
- return unowned?.Index == checkAssignment.Index
+ return (unowned != null ? unowned.Identity.Index : null) == checkAssignment.Identity.Index
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty;
}
@@ -616,7 +617,7 @@ public class ActiveCollections : ISavable, IDisposable
if (maleNpc == null)
{
maleNpc = Default;
- if (maleNpc.Index != checkAssignment.Index)
+ if (maleNpc.Identity.Index != checkAssignment.Identity.Index)
return string.Empty;
collection1 = CollectionType.Default;
@@ -625,7 +626,7 @@ public class ActiveCollections : ISavable, IDisposable
if (femaleNpc == null)
{
femaleNpc = Default;
- if (femaleNpc.Index != checkAssignment.Index)
+ if (femaleNpc.Identity.Index != checkAssignment.Identity.Index)
return string.Empty;
collection2 = CollectionType.Default;
@@ -645,7 +646,7 @@ public class ActiveCollections : ISavable, IDisposable
if (assignment == null)
continue;
- if (assignment.Index == checkAssignment.Index)
+ if (assignment.Identity.Index == checkAssignment.Identity.Index)
return
$"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
}
diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs
index 0243de1e..f62eea3f 100644
--- a/Penumbra/Collections/Manager/CollectionEditor.cs
+++ b/Penumbra/Collections/Manager/CollectionEditor.cs
@@ -1,4 +1,5 @@
-using OtterGui;
+using OtterGui.Extensions;
+using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
@@ -7,7 +8,7 @@ using Penumbra.Services;
namespace Penumbra.Collections.Manager;
-public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage)
+public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService
{
/// Enable or disable the mod inheritance of mod idx.
public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit)
@@ -25,12 +26,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
///
public bool SetModState(ModCollection collection, Mod mod, bool newValue)
{
- var oldValue = collection.Settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false;
+ var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Enabled ?? false;
if (newValue == oldValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
- ((List)collection.Settings)[mod.Index]!.Enabled = newValue;
+ collection.GetOwnSettings(mod.Index)!.Enabled = newValue;
InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True,
0);
return true;
@@ -54,13 +55,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
var changes = false;
foreach (var mod in mods)
{
- var oldValue = collection.Settings[mod.Index]?.Enabled;
+ var oldValue = collection.GetOwnSettings(mod.Index)?.Enabled;
if (newValue == oldValue)
continue;
FixInheritance(collection, mod, false);
- ((List)collection.Settings)[mod.Index]!.Enabled = newValue;
- changes = true;
+ collection.GetOwnSettings(mod.Index)!.Enabled = newValue;
+ changes = true;
}
if (!changes)
@@ -75,35 +76,64 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
///
public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue)
{
- var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default;
+ var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Priority ?? ModPriority.Default;
if (newValue == oldValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
- ((List)collection.Settings)[mod.Index]!.Priority = newValue;
+ collection.GetOwnSettings(mod.Index)!.Priority = newValue;
InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0);
return true;
}
///
/// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary.
- /// /// If the mod is currently inherited, stop the inheritance.
+ /// If the mod is currently inherited, stop the inheritance.
///
public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue)
{
- var settings = collection.Settings[mod.Index] != null
- ? collection.Settings[mod.Index]!.Settings
- : collection[mod.Index].Settings?.Settings;
+ var settings = collection.GetInheritedSettings(mod.Index).Settings?.Settings;
var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings;
if (oldValue == newValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
- ((List)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue);
+ collection.GetOwnSettings(mod.Index)!.SetValue(mod, groupIdx, newValue);
InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx);
return true;
}
+ public bool SetTemporarySettings(ModCollection collection, Mod mod, TemporaryModSettings? settings, int key = 0)
+ {
+ key = settings?.Lock ?? key;
+ if (!CanSetTemporarySettings(collection, mod, key))
+ return false;
+
+ collection.Settings.SetTemporary(mod.Index, settings);
+ InvokeChange(collection, ModSettingChange.TemporarySetting, mod, Setting.Indefinite, 0);
+ return true;
+ }
+
+ public int ClearTemporarySettings(ModCollection collection, int key = 0)
+ {
+ var numRemoved = 0;
+ for (var i = 0; i < collection.Settings.Count; ++i)
+ {
+ if (collection.GetTempSettings(i) is { } tempSettings
+ && tempSettings.Lock == key
+ && SetTemporarySettings(collection, modStorage[i], null, key))
+ ++numRemoved;
+ }
+
+ return numRemoved;
+ }
+
+ public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key)
+ {
+ var old = collection.GetTempSettings(mod.Index);
+ return old is not { Lock: > 0 } || old.Lock == key;
+ }
+
/// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist.
public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName)
{
@@ -114,10 +144,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
// If it does not exist, check unused settings.
// If it does not exist and has no unused settings, also use null.
ModSettings.SavedSettings? savedSettings = sourceMod != null
- ? collection.Settings[sourceMod.Index] != null
- ? new ModSettings.SavedSettings(collection.Settings[sourceMod.Index]!, sourceMod)
+ ? collection.GetOwnSettings(sourceMod.Index) is { } ownSettings
+ ? new ModSettings.SavedSettings(ownSettings, sourceMod)
: null
- : collection.UnusedSettings.TryGetValue(sourceName, out var s)
+ : collection.Settings.Unused.TryGetValue(sourceName, out var s)
? s
: null;
@@ -147,10 +177,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
// or remove any unused settings for the target if they are inheriting.
if (savedSettings != null)
{
- ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value;
+ ((Dictionary)collection.Settings.Unused)[targetName] = savedSettings.Value;
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
- else if (((Dictionary)collection.UnusedSettings).Remove(targetName))
+ else if (((Dictionary)collection.Settings.Unused).Remove(targetName))
{
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
@@ -165,12 +195,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
///
private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit)
{
- var settings = collection.Settings[mod.Index];
+ var settings = collection.GetOwnSettings(mod.Index);
if (inherit == (settings == null))
return false;
- ((List)collection.Settings)[mod.Index] =
- inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod);
+ var settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod);
+ collection.Settings.Set(mod.Index, settings1);
return true;
}
@@ -178,16 +208,18 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
- saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
+ if (type is not ModSettingChange.TemporarySetting)
+ saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false);
- RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx);
+ if (type is not ModSettingChange.TemporarySetting)
+ RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx);
}
/// Trigger changes in all inherited collections.
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
- foreach (var directInheritor in directParent.DirectParentOf)
+ foreach (var directInheritor in directParent.Inheritance.DirectlyInheritedBy)
{
switch (type)
{
@@ -196,7 +228,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true);
break;
default:
- if (directInheritor.Settings[mod!.Index] == null)
+ if (directInheritor.GetOwnSettings(mod!.Index) == null)
communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true);
break;
}
diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs
index e95617b1..85f5b957 100644
--- a/Penumbra/Collections/Manager/CollectionManager.cs
+++ b/Penumbra/Collections/Manager/CollectionManager.cs
@@ -1,3 +1,4 @@
+using OtterGui.Services;
using Penumbra.Collections.Cache;
namespace Penumbra.Collections.Manager;
@@ -8,7 +9,7 @@ public class CollectionManager(
InheritanceManager inheritances,
CollectionCacheManager caches,
TempCollectionManager temp,
- CollectionEditor editor)
+ CollectionEditor editor) : IService
{
public readonly CollectionStorage Storage = storage;
public readonly ActiveCollections Active = active;
diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs
index de5d0a14..531b6333 100644
--- a/Penumbra/Collections/Manager/CollectionStorage.cs
+++ b/Penumbra/Collections/Manager/CollectionStorage.cs
@@ -1,6 +1,7 @@
-using Dalamud.Interface.Internal.Notifications;
-using OtterGui;
+using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
+using OtterGui.Extensions;
+using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
@@ -13,20 +14,67 @@ using Penumbra.Services;
namespace Penumbra.Collections.Manager;
-public class CollectionStorage : IReadOnlyList, IDisposable
+/// A contiguously incrementing ID managed by the CollectionCreator.
+public readonly record struct LocalCollectionId(int Id) : IAdditionOperators
+{
+ public static readonly LocalCollectionId Zero = new(0);
+
+ public static LocalCollectionId operator +(LocalCollectionId left, int right)
+ => new(left.Id + right);
+}
+
+public class CollectionStorage : IReadOnlyList, IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
+ public ModCollection Create(string name, int index, ModCollection? duplicate)
+ {
+ var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
+ ?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
+ _collectionsByLocal[CurrentCollectionId] = newCollection;
+ CurrentCollectionId += 1;
+ return newCollection;
+ }
+
+ public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings,
+ IReadOnlyList inheritances)
+ {
+ var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
+ new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
+ _collectionsByLocal[CurrentCollectionId] = newCollection;
+ CurrentCollectionId += 1;
+ return newCollection;
+ }
+
+ public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
+ {
+ var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
+ _collectionsByLocal[CurrentCollectionId] = newCollection;
+ CurrentCollectionId += 1;
+ return newCollection;
+ }
+
+ public void Delete(ModCollection collection)
+ => _collectionsByLocal.Remove(collection.Identity.LocalId);
+
/// The empty collection is always available at Index 0.
private readonly List _collections =
[
ModCollection.Empty,
];
+ /// A list of all collections ever created still existing by their local id.
+ private readonly Dictionary
+ _collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
+
+
public readonly ModCollection DefaultNamed;
+ /// Incremented by 1 because the empty collection gets Zero.
+ public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
+
/// Default enumeration skips the empty collection.
public IEnumerator GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
@@ -44,7 +92,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
{
if (name.Length != 0)
- return _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
+ return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
collection = ModCollection.Empty;
return true;
@@ -54,7 +102,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
{
if (id != Guid.Empty)
- return _collections.FindFirst(c => c.Id == id, out collection);
+ return _collections.FindFirst(c => c.Identity.Id == id, out collection);
collection = ModCollection.Empty;
return true;
@@ -69,6 +117,10 @@ public class CollectionStorage : IReadOnlyList, IDisposable
return ByName(identifier, out collection);
}
+ /// Find a collection by its local ID if it still exists, otherwise returns the empty collection.
+ public ModCollection ByLocalId(LocalCollectionId localId)
+ => _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
+
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
@@ -100,12 +152,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable
///
public bool AddCollection(string name, ModCollection? duplicate)
{
- var newCollection = duplicate?.Duplicate(name, _collections.Count)
- ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
- _collections.Add(newCollection);
+ if (name.Length == 0)
+ return false;
+ var newCollection = Create(name, _collections.Count, duplicate);
+ _collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
- Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false);
+ Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
return true;
}
@@ -115,42 +168,48 @@ public class CollectionStorage : IReadOnlyList, IDisposable
///
public bool RemoveCollection(ModCollection collection)
{
- if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count)
+ if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
{
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
return false;
}
- if (collection.Index == DefaultNamed.Index)
+ if (collection.Identity.Index == DefaultNamed.Identity.Index)
{
Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false);
return false;
}
+ Delete(collection);
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
- _collections.RemoveAt(collection.Index);
+ _collections.RemoveAt(collection.Identity.Index);
// Update indices.
- for (var i = collection.Index; i < Count; ++i)
- _collections[i].Index = i;
+ for (var i = collection.Identity.Index; i < Count; ++i)
+ _collections[i].Identity.Index = i;
+ _collectionsByLocal.Remove(collection.Identity.LocalId);
- Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false);
+ Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true;
}
/// Remove all settings for not currently-installed mods from the given collection.
- public void CleanUnavailableSettings(ModCollection collection)
+ public int CleanUnavailableSettings(ModCollection collection)
{
- var any = collection.UnusedSettings.Count > 0;
- ((Dictionary)collection.UnusedSettings).Clear();
- if (any)
+ var count = collection.Settings.Unused.Count;
+ if (count > 0)
+ {
+ ((Dictionary)collection.Settings.Unused).Clear();
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
+ }
+
+ return count;
}
/// Remove a specific setting for not currently-installed mods from the given collection.
public void CleanUnavailableSetting(ModCollection collection, string? setting)
{
- if (setting != null && ((Dictionary)collection.UnusedSettings).Remove(setting))
+ if (setting != null && ((Dictionary)collection.Settings.Unused).Remove(setting))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
@@ -180,7 +239,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable
continue;
}
- var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance);
+ var collection = CreateFromData(id, name, version, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
try
@@ -191,13 +250,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage(
- $"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.",
+ $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
NotificationType.Warning);
}
catch (Exception ex)
{
Penumbra.Messager.NotificationMessage(
- $"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}",
+ $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
NotificationType.Warning);
}
}
@@ -218,7 +277,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
- $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.",
+ $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
NotificationType.Error);
}
@@ -236,14 +295,14 @@ public class CollectionStorage : IReadOnlyList, IDisposable
///
private ModCollection SetDefaultNamedCollection()
{
- if (ByName(ModCollection.DefaultCollectionName, out var collection))
+ if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
return collection;
- if (AddCollection(ModCollection.DefaultCollectionName, null))
+ if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
return _collections[^1];
Penumbra.Messager.NotificationMessage(
- $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.",
+ $"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.",
NotificationType.Error);
return Count > 1 ? _collections[1] : _collections[0];
}
@@ -252,7 +311,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable
private void OnModDiscoveryStarted()
{
foreach (var collection in this)
- collection.PrepareModDiscovery(_modStorage);
+ collection.Settings.PrepareModDiscovery(_modStorage);
}
/// Restore all settings in all collections to mods.
@@ -260,7 +319,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable
{
// Re-apply all mod settings.
foreach (var collection in this)
- collection.ApplyModSettings(_saveService, _modStorage);
+ collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
}
/// Add or remove a mod from all collections, or re-save all collections where the mod has settings.
@@ -271,21 +330,22 @@ public class CollectionStorage : IReadOnlyList, IDisposable
{
case ModPathChangeType.Added:
foreach (var collection in this)
- collection.AddMod(mod);
+ collection.Settings.AddMod(mod);
break;
case ModPathChangeType.Deleted:
foreach (var collection in this)
- collection.RemoveMod(mod);
+ collection.Settings.RemoveMod(mod);
break;
case ModPathChangeType.Moved:
- foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
+ foreach (var collection in this.Where(collection => collection.GetOwnSettings(mod.Index) != null))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
break;
case ModPathChangeType.Reloaded:
foreach (var collection in this)
{
- if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false)
+ if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
+ collection.Settings.SetTemporary(mod.Index, null);
}
break;
@@ -293,7 +353,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable
}
/// Save all collections where the mod has settings and the change requires saving.
- private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx)
+ private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
+ int movedToIdx)
{
type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)
@@ -301,8 +362,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable
foreach (var collection in this)
{
- if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
+ if (collection.GetOwnSettings(mod.Index)?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
+ collection.Settings.SetTemporary(mod.Index, null);
}
}
@@ -314,9 +376,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable
foreach (var collection in this)
{
- var (settings, _) = collection[mod.Index];
+ var (settings, _) = collection.GetActualSettings(mod.Index);
if (settings is { Enabled: true })
- collection.IncrementCounter();
+ collection.Counters.IncrementChange();
}
}
}
diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs
index 785f0013..d0a70630 100644
--- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs
+++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs
@@ -48,8 +48,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
// Handle generic NPC
var npcIdentifier = _actors.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty,
- ushort.MaxValue,
- identifier.Kind, identifier.DataId);
+ ushort.MaxValue, identifier.Kind, identifier.DataId);
if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection))
return true;
@@ -58,8 +57,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
return false;
identifier = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName,
- identifier.HomeWorld.Id,
- ObjectKind.None, uint.MaxValue);
+ identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue);
return CheckWorlds(identifier, out collection);
}
case IdentifierType.Npc: return _individuals.TryGetValue(identifier, out collection);
@@ -127,7 +125,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
}
}
- public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection)
+ public bool TryGetCollection(IGameObject? gameObject, out ModCollection? collection)
=> TryGetCollection(_actors.FromObject(gameObject, true, false, false), out collection);
public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection)
diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs
index 8a717b35..60e9fc5f 100644
--- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs
+++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs
@@ -1,5 +1,5 @@
using Dalamud.Game.ClientState.Objects.Enums;
-using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Interface.ImGuiNotification;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
@@ -18,7 +18,7 @@ public partial class IndividualCollections
foreach (var (name, identifiers, collection) in Assignments)
{
var tmp = identifiers[0].ToJson();
- tmp.Add("Collection", collection.Id);
+ tmp.Add("Collection", collection.Identity.Id);
tmp.Add("Display", name);
ret.Add(tmp);
}
@@ -182,7 +182,7 @@ public partial class IndividualCollections
Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}].");
else
Penumbra.Messager.NotificationMessage(
- $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.",
+ $"Could not migrate {name} ({collection.Identity.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.",
NotificationType.Error);
}
// If it is not a valid NPC name, check if it can be a player name.
@@ -192,16 +192,16 @@ public partial class IndividualCollections
var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}."));
// Try to migrate the player name without logging full names.
if (Add($"{name} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", [identifier], collection))
- Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier.");
+ Penumbra.Log.Information($"Migrated {shortName} ({collection.Identity.AnonymizedName}) to Player Identifier.");
else
Penumbra.Messager.NotificationMessage(
- $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.",
+ $"Could not migrate {shortName} ({collection.Identity.AnonymizedName}), please look through your individual collections.",
NotificationType.Error);
}
else
{
Penumbra.Messager.NotificationMessage(
- $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.",
+ $"Could not migrate {name} ({collection.Identity.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.",
NotificationType.Error);
}
}
diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs
index 6003b5f9..34582677 100644
--- a/Penumbra/Collections/Manager/InheritanceManager.cs
+++ b/Penumbra/Collections/Manager/InheritanceManager.cs
@@ -1,12 +1,10 @@
-using Dalamud.Interface.Internal.Notifications;
-using OtterGui;
+using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
-using OtterGui.Filesystem;
+using OtterGui.Extensions;
+using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Services;
-using Penumbra.UI.CollectionTab;
-using Penumbra.Util;
namespace Penumbra.Collections.Manager;
@@ -15,7 +13,7 @@ namespace Penumbra.Collections.Manager;
/// This is transitive, so a collection A inheriting from B also inherits from everything B inherits.
/// Circular dependencies are resolved by distinctness.
///
-public class InheritanceManager : IDisposable
+public class InheritanceManager : IDisposable, IService
{
public enum ValidInheritance
{
@@ -64,10 +62,10 @@ public class InheritanceManager : IDisposable
if (ReferenceEquals(potentialParent, potentialInheritor))
return ValidInheritance.Self;
- if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent))
+ if (potentialInheritor.Inheritance.DirectlyInheritsFrom.Contains(potentialParent))
return ValidInheritance.Contained;
- if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor)))
+ if (potentialParent.Inheritance.FlatHierarchy.Any(c => ReferenceEquals(c, potentialInheritor)))
return ValidInheritance.Circle;
return ValidInheritance.Valid;
@@ -84,25 +82,23 @@ public class InheritanceManager : IDisposable
/// Remove an existing inheritance from a collection.
public void RemoveInheritance(ModCollection inheritor, int idx)
{
- var parent = inheritor.DirectlyInheritsFrom[idx];
- ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx);
- ((List)parent.DirectParentOf).Remove(inheritor);
+ var parent = inheritor.Inheritance.RemoveInheritanceAt(inheritor, idx);
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
- RecurseInheritanceChanges(inheritor);
- Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances.");
+ RecurseInheritanceChanges(inheritor, true);
+ Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.AnonymizedName} inheritances.");
}
/// Order in the inheritance list is relevant.
public void MoveInheritance(ModCollection inheritor, int from, int to)
{
- if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to))
+ if (!inheritor.Inheritance.MoveInheritance(inheritor, from, to))
return;
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
- RecurseInheritanceChanges(inheritor);
- Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}.");
+ RecurseInheritanceChanges(inheritor, true);
+ Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}.");
}
///
@@ -111,16 +107,16 @@ public class InheritanceManager : IDisposable
if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid)
return false;
- ((List)inheritor.DirectlyInheritsFrom).Add(parent);
- ((List)parent.DirectParentOf).Add(inheritor);
+ inheritor.Inheritance.AddInheritance(inheritor, parent);
if (invokeEvent)
{
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
- RecurseInheritanceChanges(inheritor);
}
- Penumbra.Log.Debug($"Added {parent.AnonymizedName} to {inheritor.AnonymizedName} inheritances.");
+ RecurseInheritanceChanges(inheritor, invokeEvent);
+
+ Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances.");
return true;
}
@@ -132,11 +128,11 @@ public class InheritanceManager : IDisposable
{
foreach (var collection in _storage)
{
- if (collection.InheritanceByName == null)
+ if (collection.Inheritance.ConsumeNames() is not { } byName)
continue;
var changes = false;
- foreach (var subCollectionName in collection.InheritanceByName)
+ foreach (var subCollectionName in byName)
{
if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection))
{
@@ -144,26 +140,30 @@ public class InheritanceManager : IDisposable
continue;
changes = true;
- Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
+ Penumbra.Messager.NotificationMessage(
+ $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.",
+ NotificationType.Warning);
}
else if (_storage.ByName(subCollectionName, out subCollection))
{
changes = true;
- Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID.");
+ Penumbra.Log.Information($"Migrating inheritance for {collection.Identity.AnonymizedName} from name to GUID.");
if (AddInheritance(collection, subCollection, false))
continue;
- Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
+ Penumbra.Messager.NotificationMessage(
+ $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.",
+ NotificationType.Warning);
}
else
{
Penumbra.Messager.NotificationMessage(
- $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", NotificationType.Warning);
+ $"Inherited collection {subCollectionName} for {collection.Identity.AnonymizedName} does not exist, it was removed.",
+ NotificationType.Warning);
changes = true;
}
}
- collection.InheritanceByName = null;
if (changes)
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection));
}
@@ -176,20 +176,22 @@ public class InheritanceManager : IDisposable
foreach (var c in _storage)
{
- var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old);
+ var inheritedIdx = c.Inheritance.DirectlyInheritsFrom.IndexOf(old);
if (inheritedIdx >= 0)
RemoveInheritance(c, inheritedIdx);
- ((List)c.DirectParentOf).Remove(old);
+ c.Inheritance.RemoveChild(old);
}
}
- private void RecurseInheritanceChanges(ModCollection newInheritor)
+ private void RecurseInheritanceChanges(ModCollection newInheritor, bool invokeEvent)
{
- foreach (var inheritor in newInheritor.DirectParentOf)
+ foreach (var inheritor in newInheritor.Inheritance.DirectlyInheritedBy)
{
- _communicator.CollectionInheritanceChanged.Invoke(inheritor, true);
- RecurseInheritanceChanges(inheritor);
+ ModCollectionInheritance.UpdateFlattenedInheritance(inheritor);
+ RecurseInheritanceChanges(inheritor, invokeEvent);
+ if (invokeEvent)
+ _communicator.CollectionInheritanceChanged.Invoke(inheritor, true);
}
}
}
diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs
index fe61285d..7db375f7 100644
--- a/Penumbra/Collections/Manager/ModCollectionMigration.cs
+++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs
@@ -26,12 +26,12 @@ internal static class ModCollectionMigration
// Remove all completely defaulted settings from active and inactive mods.
for (var i = 0; i < collection.Settings.Count; ++i)
{
- if (SettingIsDefaultV0(collection.Settings[i]))
- ((List)collection.Settings)[i] = null;
+ if (SettingIsDefaultV0(collection.GetOwnSettings(i)))
+ collection.Settings.SetAll(i, FullModSettings.Empty);
}
- foreach (var (key, _) in collection.UnusedSettings.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList())
- ((Dictionary)collection.UnusedSettings).Remove(key);
+ foreach (var (key, _) in collection.Settings.Unused.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList())
+ collection.Settings.RemoveUnused(key);
return true;
}
diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs
index de08c6a2..9476e38c 100644
--- a/Penumbra/Collections/Manager/TempCollectionManager.cs
+++ b/Penumbra/Collections/Manager/TempCollectionManager.cs
@@ -1,4 +1,5 @@
-using OtterGui;
+using OtterGui.Extensions;
+using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
@@ -8,7 +9,7 @@ using Penumbra.String;
namespace Penumbra.Collections.Manager;
-public class TempCollectionManager : IDisposable
+public class TempCollectionManager : IDisposable, IService
{
public int GlobalChangeCounter { get; private set; }
public readonly IndividualCollections Collections;
@@ -43,7 +44,7 @@ public class TempCollectionManager : IDisposable
=> _customCollections.Values;
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
- => _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection);
+ => _customCollections.Values.FindFirst(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase), out collection);
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(id, out collection);
@@ -52,13 +53,13 @@ public class TempCollectionManager : IDisposable
{
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
- var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++);
- Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}.");
- if (_customCollections.TryAdd(collection.Id, collection))
+ var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++);
+ Penumbra.Log.Debug($"Creating temporary collection {collection.Identity.Name} with {collection.Identity.Id}.");
+ if (_customCollections.TryAdd(collection.Identity.Id, collection))
{
// Temporary collection created.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty);
- return collection.Id;
+ return collection.Identity.Id;
}
return Guid.Empty;
@@ -72,8 +73,9 @@ public class TempCollectionManager : IDisposable
return false;
}
- Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}.");
- GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
+ _storage.Delete(collection);
+ Penumbra.Log.Debug($"Deleted temporary collection {collection.Identity.Id}.");
+ GlobalChangeCounter += Math.Max(collection.Counters.Change + 1 - GlobalChangeCounter, 0);
for (var i = 0; i < Collections.Count; ++i)
{
if (Collections[i].Collection != collection)
@@ -81,7 +83,7 @@ public class TempCollectionManager : IDisposable
// Temporary collection assignment removed.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
- Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}.");
+ Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Identity.Id} from {Collections[i].DisplayName}.");
Collections.Delete(i--);
}
@@ -94,7 +96,7 @@ public class TempCollectionManager : IDisposable
return false;
// Temporary collection assignment added.
- Penumbra.Log.Verbose($"Assigned temporary collection {collection.AnonymizedName} to {Collections.Last().DisplayName}.");
+ Penumbra.Log.Verbose($"Assigned temporary collection {collection.Identity.AnonymizedName} to {Collections.Last().DisplayName}.");
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName);
return true;
}
@@ -125,6 +127,6 @@ public class TempCollectionManager : IDisposable
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
- return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id);
+ return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Identity.Id);
}
}
diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs
index 3f3733e0..716b153e 100644
--- a/Penumbra/Collections/ModCollection.Cache.Access.cs
+++ b/Penumbra/Collections/ModCollection.Cache.Access.cs
@@ -1,14 +1,9 @@
using OtterGui.Classes;
-using Penumbra.GameData.Enums;
using Penumbra.Mods;
-using Penumbra.Interop.Structs;
-using Penumbra.Meta.Files;
-using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.Collections.Cache;
-using Penumbra.Interop.Services;
+using Penumbra.GameData.Data;
using Penumbra.Mods.Editor;
-using Penumbra.GameData.Structs;
namespace Penumbra.Collections;
@@ -48,74 +43,15 @@ public partial class ModCollection
internal MetaCache? MetaCache
=> _cache?.Meta;
- public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
- {
- if (_cache != null)
- return _cache.Meta.GetImcFile(path, out file);
-
- file = null;
- return false;
- }
-
internal IReadOnlyDictionary ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary();
- internal IReadOnlyDictionary, object?)> ChangedItems
- => _cache?.ChangedItems ?? new Dictionary, object?)>();
+ internal IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems
+ => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>();
internal IEnumerable> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty>();
internal SingleArray Conflicts(Mod mod)
=> _cache?.Conflicts(mod) ?? new SingleArray();
-
- public void SetFiles(CharacterUtility utility)
- {
- if (_cache == null)
- {
- utility.ResetAll();
- }
- else
- {
- _cache.Meta.SetFiles();
- Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Identifier}.");
- }
- }
-
- public void SetMetaFile(CharacterUtility utility, MetaIndex idx)
- {
- if (_cache == null)
- utility.ResetResource(idx);
- else
- _cache.Meta.SetFile(idx);
- }
-
- // Used for short periods of changed files.
- public MetaList.MetaReverter? TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory)
- {
- if (_cache != null)
- return _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory);
-
- var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
- return idx >= 0 ? utility.TemporarilyResetResource(idx) : null;
- }
-
- public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility)
- => _cache?.Meta.TemporarilySetEqpFile()
- ?? utility.TemporarilyResetResource(MetaIndex.Eqp);
-
- public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility)
- => _cache?.Meta.TemporarilySetGmpFile()
- ?? utility.TemporarilyResetResource(MetaIndex.Gmp);
-
- public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility)
- => _cache?.Meta.TemporarilySetCmpFile()
- ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp);
-
- public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type)
- => _cache?.Meta.TemporarilySetEstFile(type)
- ?? utility.TemporarilyResetResource((MetaIndex)type);
-
- public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor)
- => _cache?.Meta.ApplyGlobalEqp(baseEntry, armor) ?? baseEntry;
}
diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs
index 4580e37a..69f82458 100644
--- a/Penumbra/Collections/ModCollection.cs
+++ b/Penumbra/Collections/ModCollection.cs
@@ -1,4 +1,3 @@
-using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Settings;
@@ -13,203 +12,129 @@ namespace Penumbra.Collections;
/// - Index is the collections index in the ModCollection.Manager
/// - Settings has the same size as ModManager.Mods.
/// - any change in settings or inheritance of the collection causes a Save.
-/// - the name can not contain invalid path characters and has to be unique when lower-cased.
///
public partial class ModCollection
{
- public const int CurrentVersion = 2;
- public const string DefaultCollectionName = "Default";
- public const string EmptyCollectionName = "None";
+ public const int CurrentVersion = 2;
///
/// Create the always available Empty Collection that will always sit at index 0,
/// can not be deleted and does never create a cache.
///
- public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, 0, 0, CurrentVersion, [], [], []);
+ public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, new ModSettingProvider(),
+ new ModCollectionInheritance());
- /// The name of a collection.
- public string Name { get; set; }
-
- public Guid Id { get; }
-
- public string Identifier
- => Id.ToString();
-
- public string ShortIdentifier
- => Identifier[..8];
+ public ModCollectionIdentity Identity;
public override string ToString()
- => Name.Length > 0 ? Name : ShortIdentifier;
+ => Identity.ToString();
- /// Get the first two letters of a collection name and its Index (or None if it is the empty collection).
- public string AnonymizedName
- => this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier;
+ public readonly ModSettingProvider Settings;
+ public ModCollectionInheritance Inheritance;
+ public CollectionCounters Counters;
- /// The index of the collection is set and kept up-to-date by the CollectionManager.
- public int Index { get; internal set; }
- ///
- /// Count the number of changes of the effective file list.
- /// This is used for material and imc changes.
- ///
- public int ChangeCounter { get; private set; }
-
- /// Increment the number of changes in the effective file list.
- public int IncrementCounter()
- => ++ChangeCounter;
-
- ///
- /// If a ModSetting is null, it can be inherited from other collections.
- /// If no collection provides a setting for the mod, it is just disabled.
- ///
- public readonly IReadOnlyList Settings;
-
- /// Settings for deleted mods will be kept via the mods identifier (directory name).
- public readonly IReadOnlyDictionary UnusedSettings;
-
- /// Inheritances stored before they can be applied.
- public IReadOnlyList? InheritanceByName;
-
- /// Contains all direct parent collections this collection inherits settings from.
- public readonly IReadOnlyList DirectlyInheritsFrom;
-
- /// Contains all direct child collections that inherit from this collection.
- public readonly IReadOnlyList DirectParentOf = new List();
-
- /// All inherited collections in application order without filtering for duplicates.
- public static IEnumerable InheritedCollections(ModCollection collection)
- => collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection);
-
- ///
- /// Iterate over all collections inherited from in depth-first order.
- /// Skip already visited collections to avoid circular dependencies.
- ///
- public IEnumerable GetFlattenedInheritance()
- => InheritedCollections(this).Distinct();
-
- ///
- /// Obtain the actual settings for a given mod via index.
- /// Also returns the collection the settings are taken from.
- /// If no collection provides settings for this mod, this collection is returned together with null.
- ///
- public (ModSettings? Settings, ModCollection Collection) this[Index idx]
+ public ModSettings? GetOwnSettings(Index idx)
{
- get
+ if (Identity.Index <= 0)
+ return ModSettings.Empty;
+
+ return Settings.Settings[idx].Settings;
+ }
+
+ public TemporaryModSettings? GetTempSettings(Index idx)
+ {
+ if (Identity.Index <= 0)
+ return null;
+
+ return Settings.Settings[idx].TempSettings;
+ }
+
+ public (ModSettings? Settings, ModCollection Collection) GetInheritedSettings(Index idx)
+ {
+ if (Identity.Index <= 0)
+ return (ModSettings.Empty, this);
+
+ foreach (var collection in Inheritance.FlatHierarchy)
{
- if (Index <= 0)
- return (ModSettings.Empty, this);
-
- foreach (var collection in GetFlattenedInheritance())
- {
- var settings = collection.Settings[idx];
- if (settings != null)
- return (settings, collection);
- }
-
- return (null, this);
+ var settings = collection.Settings.Settings[idx].Settings;
+ if (settings != null)
+ return (settings, collection);
}
+
+ return (null, this);
+ }
+
+ public (ModSettings? Settings, ModCollection Collection) GetActualSettings(Index idx)
+ {
+ if (Identity.Index <= 0)
+ return (ModSettings.Empty, this);
+
+ // Check temp settings.
+ var ownTempSettings = Settings.Settings[idx].Resolve();
+ if (ownTempSettings != null)
+ return (ownTempSettings, this);
+
+ // Ignore temp settings for inherited collections.
+ foreach (var collection in Inheritance.FlatHierarchy.Skip(1))
+ {
+ var settings = collection.Settings.Settings[idx].Settings;
+ if (settings != null)
+ return (settings, collection);
+ }
+
+ return (null, this);
}
/// Evaluates all settings along the whole inheritance tree.
public IEnumerable ActualSettings
- => Enumerable.Range(0, Settings.Count).Select(i => this[i].Settings);
+ => Enumerable.Range(0, Settings.Count).Select(i => GetActualSettings(i).Settings);
///
/// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists.
///
- public ModCollection Duplicate(string name, int index)
+ public ModCollection Duplicate(string name, LocalCollectionId localId, int index)
{
Debug.Assert(index > 0, "Collection duplicated with non-positive index.");
- return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
- [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()));
+ return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, Settings.Clone(), Inheritance.Clone());
}
/// Constructor for reading from files.
- public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index,
+ public static ModCollection CreateFromData(SaveService saver, ModStorage mods, ModCollectionIdentity identity, int version,
Dictionary allSettings, IReadOnlyList inheritances)
{
- Debug.Assert(index > 0, "Collection read with non-positive index.");
- var ret = new ModCollection(id, name, index, 0, version, [], [], allSettings)
- {
- InheritanceByName = inheritances,
- };
- ret.ApplyModSettings(saver, mods);
+ Debug.Assert(identity.Index > 0, "Collection read with non-positive index.");
+ var ret = new ModCollection(identity, 0, version, new ModSettingProvider(allSettings), new ModCollectionInheritance(inheritances));
+ ret.Settings.ApplyModSettings(ret, saver, mods);
ModCollectionMigration.Migrate(saver, mods, version, ret);
return ret;
}
/// Constructor for temporary collections.
- public static ModCollection CreateTemporary(string name, int index, int changeCounter)
+ public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter)
{
Debug.Assert(index < 0, "Temporary collection created with non-negative index.");
- var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []);
+ var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, new ModSettingProvider(),
+ new ModCollectionInheritance());
return ret;
}
/// Constructor for empty collections.
- public static ModCollection CreateEmpty(string name, int index, int modCount)
+ public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount)
{
Debug.Assert(index >= 0, "Empty collection created with negative index.");
- return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [],
- []);
+ return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, ModSettingProvider.Empty(modCount),
+ new ModCollectionInheritance());
}
- /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
- internal bool AddMod(Mod mod)
+ private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, ModSettingProvider settings,
+ ModCollectionInheritance inheritance)
{
- if (UnusedSettings.TryGetValue(mod.ModPath.Name, out var save))
- {
- var ret = save.ToSettings(mod, out var settings);
- ((List)Settings).Add(settings);
- ((Dictionary)UnusedSettings).Remove(mod.ModPath.Name);
- return ret;
- }
-
- ((List)Settings).Add(null);
- return false;
- }
-
- /// Move settings from the current mod list to the unused mod settings.
- internal void RemoveMod(Mod mod)
- {
- var settings = Settings[mod.Index];
- if (settings != null)
- ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod);
-
- ((List