Compare commits

..

No commits in common. "master" and "testing_1.0.3.2" have entirely different histories.

507 changed files with 12442 additions and 33928 deletions

View file

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

View file

@ -10,15 +10,13 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
10.x.x
9.x.x
dotnet-version: '8.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
@ -31,7 +29,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Penumbra/bin/Release/*

View file

@ -9,20 +9,18 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
10.x.x
9.x.x
dotnet-version: '8.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |
@ -39,7 +37,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Penumbra/bin/Release/*

View file

@ -9,20 +9,18 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
10.x.x
9.x.x
dotnet-version: '8.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |
@ -39,7 +37,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Penumbra/bin/Debug/*

@ -1 +1 @@
Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf
Subproject commit 1d9365164655a7cb38172e1311e15e19b1def6db

@ -1 +1 @@
Subproject commit 52a3216a525592205198303df2844435e382cf87
Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
@ -57,10 +55,8 @@ 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();
}
}

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
@ -40,10 +38,8 @@ 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();
}
}

View file

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

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
@ -46,16 +44,12 @@ 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();
}
}

View file

@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using Penumbra.CrashHandler.Buffers;
namespace Penumbra.CrashHandler;
@ -57,7 +55,7 @@ public class CrashData
/// <summary> The last vfx function invoked before this crash data was generated. </summary>
public VfxFuncInvokedEntry? LastVfxFuncInvoked
=> LastVFXFuncsInvoked.Count == 0 ? default : LastVFXFuncsInvoked[0];
=> LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0];
/// <summary> A collection of the last few characters loaded before this crash data was generated. </summary>
public List<CharacterLoadedEntry> LastCharactersLoaded { get; set; } = [];
@ -66,5 +64,5 @@ public class CrashData
public List<ModdedFileLoadedEntry> LastModdedFilesLoaded { get; set; } = [];
/// <summary> A collection of the last few vfx functions invoked before this crash data was generated. </summary>
public List<VfxFuncInvokedEntry> LastVFXFuncsInvoked { get; set; } = [];
public List<VfxFuncInvokedEntry> LastVfxFuncsInvoked { get; set; } = [];
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +0,0 @@
{
"version": 1,
"dependencies": {
"net10.0-windows7.0": {
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.2.39, )",
"resolved": "1.2.39",
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
}
}
}
}

@ -1 +1 @@
Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60
Subproject commit f2cea65b83b2d6cb0d03339e8f76aed8102a41d5

@ -1 +1 @@
Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592
Subproject commit caa58c5c92710e69ce07b9d736ebe2d228cb4488

View file

@ -8,11 +8,6 @@ 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}"
@ -23,76 +18,42 @@ 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("{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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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
{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
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

View file

@ -2,14 +2,13 @@ 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<Guid, string> GetCollections()
=> collections.Storage.ToDictionary(c => c.Identity.Id, c => c.Identity.Name);
=> collections.Storage.ToDictionary(c => c.Id, c => c.Name);
public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier)
{
@ -18,33 +17,17 @@ 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.Identity.Id, collection.Identity.Name));
list.Add((collection.Id, collection.Name));
else if (identifier.Length >= 8)
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 => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase))
.Select(c => (c.Id, c.Name)));
list.AddRange(collections.Storage
.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)));
.Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name)))
.Select(c => (c.Id, c.Name)));
return list;
}
public Func<string, (string ModDirectory, string ModName)[]> CheckCurrentChangedItemFunc()
{
var weakRef = new WeakReference<CollectionManager>(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<string, object?> GetChangedItemsForCollection(Guid collectionId)
{
try
@ -53,7 +36,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?.ToInternalObject());
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2);
Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
return [];
@ -71,7 +54,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
return null;
var collection = collections.Active.ByType((CollectionType)type);
return collection == null ? null : (collection.Identity.Id, collection.Identity.Name);
return collection == null ? null : (collection.Id, collection.Name);
}
internal (Guid Id, string Name)? GetCollection(byte type)
@ -81,18 +64,17 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (false, false, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name));
if (collections.Active.Individuals.TryGetValue(id, out var collection))
return (true, true, (collection.Identity.Id, collection.Identity.Name));
return (true, true, (collection.Id, collection.Name));
helpers.AssociatedCollection(gameObjectIdx, out collection);
return (true, false, (collection.Identity.Id, collection.Identity.Name));
return (true, false, (collection.Id, collection.Name));
}
public Guid[] GetCollectionByName(string name)
=> collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id)
.ToArray();
=> collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray();
public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId,
bool allowCreateNew, bool allowDelete)
@ -101,7 +83,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
return (PenumbraApiEc.InvalidArgument, null);
var oldCollection = collections.Active.ByType((CollectionType)type);
var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
@ -124,7 +106,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
collections.Active.CreateSpecialCollection((CollectionType)type);
}
else if (old.Value.Item1 == collection.Identity.Id)
else if (old.Value.Item1 == collection.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}
@ -138,10 +120,10 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
{
var id = helpers.AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name));
return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name));
var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null;
var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple<Guid, string>?();
var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple<Guid, string>?();
if (collectionId == null)
{
if (old == null)
@ -166,7 +148,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.Identity.Id)
else if (old.Value.Item1 == collection.Id)
{
return (PenumbraApiEc.NothingChanged, old);
}

View file

@ -10,7 +10,6 @@ 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),
@ -19,12 +18,6 @@ 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}.")),
};
@ -33,7 +26,6 @@ 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),
@ -42,12 +34,6 @@ 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

View file

@ -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,27 +14,23 @@ 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, DrawObjectState drawObjectState)
ResourceLoader resourceLoader)
{
_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);
}
@ -63,36 +59,12 @@ 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, (Id: data.ModCollection.Identity.Id, Name: data.ModCollection.Identity.Name));
return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name));
}
public int GetCutsceneParentIndex(int actorIdx)
=> _cutsceneService.GetParentIndex(actorIdx);
public Func<int, int> GetCutsceneParentIndexFunc()
{
var weakRef = new WeakReference<CutsceneService>(_cutsceneService);
return idx =>
{
if (!weakRef.TryGetTarget(out var c))
throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed.");
return c.GetParentIndex(idx);
};
}
public Func<nint, nint> GetGameObjectFromDrawObjectFunc()
{
var weakRef = new WeakReference<DrawObjectState>(_drawObjectState);
return model =>
{
if (!weakRef.TryGetTarget(out var c))
throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed.");
return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero;
};
}
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
? PenumbraApiEc.Success
@ -100,24 +72,11 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
private unsafe void OnResourceLoaded(ResourceHandle* handle, 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 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);
}
if (resolveData.AssociatedGameObject != nint.Zero)
GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(),
manipulatedPath?.ToString() ?? originalPath.ToString());
}
private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject)
=> CreatedCharacterBase?.Invoke(gameObject, collection.Identity.Id, drawObject);
=> CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject);
}

View file

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

View file

@ -1,544 +1,23 @@
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(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers)
: IPenumbraApiMeta, IApiService
public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
{
public string GetPlayerMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
return CompressMetaManipulations(collection);
var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
}
public string GetMetaManipulations(int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
return CompressMetaManipulations(collection);
}
public Task<string> GetPlayerMetaManipulationsAsync()
{
return Task.Run(async () =>
{
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
return CompressMetaManipulations(playerCollection);
});
}
public Task<string> 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<ImcIdentifier, ImcEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair<EqpIdentifier, EqpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair<EqdpIdentifier, EqdpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair<ShpIdentifier, ShpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair<AtrIdentifier, AtrEntry>(kvp.Key, kvp.Value.Entry)));
}
return Functions.ToCompressedBase64(array, 0);
}
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<byte>(&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<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> 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<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
}
}
finally
{
cache.GlobalEqp.ExitReadLock();
}
WriteCache(zipStream, cache.Atch, AtchKey);
WriteCache(zipStream, cache.Shp, ShpKey);
WriteCache(zipStream, cache.Atr, AtrKey);
}
}
ms.Flush();
ms.Position = 0;
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
return Convert.ToBase64String(data);
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache, uint label)
where TKey : unmanaged, IMetaIdentifier
where TValue : unmanaged
{
metaCache.EnterReadLock();
try
{
if (metaCache.Count <= 0)
return;
stream.Write(label);
stream.Write(metaCache.Count);
foreach (var (identifier, (_, value)) in metaCache)
{
stream.Write(identifier);
stream.Write(value);
}
}
finally
{
metaCache.ExitReadLock();
}
}
}
/// <summary>
/// Convert manipulations from a transmitted base64 string to actual manipulations.
/// The empty string is treated as an empty set.
/// Only returns true if all conversions are successful and distinct.
/// </summary>
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<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (!data.StartsWith("META0002"u8))
{
Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix.");
manips = null;
return false;
}
manips = new MetaDictionary();
var r = new SpanBinaryReader(data[8..]);
while (r.Remaining > 4)
{
var prefix = r.ReadUInt32();
var count = r.Remaining > 4 ? r.ReadInt32() : 0;
if (count is 0)
continue;
switch (prefix)
{
case ImcKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<ImcIdentifier>();
var value = r.Read<ImcEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EqpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EqpIdentifier>();
var value = r.Read<EqpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EqdpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EqdpIdentifier>();
var value = r.Read<EqdpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case EstKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<EstIdentifier>();
var value = r.Read<EstEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case RspKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<RspIdentifier>();
var value = r.Read<RspEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case GmpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<GmpIdentifier>();
var value = r.Read<GmpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case GeqpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<GlobalEqpManipulation>();
if (!identifier.Validate() || !manips.TryAdd(identifier))
return false;
}
break;
case AtchKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<AtchIdentifier>();
var value = r.Read<AtchEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case ShpKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<ShpIdentifier>();
var value = r.Read<ShpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
case AtrKey:
for (var i = 0; i < count; ++i)
{
var identifier = r.Read<AtrIdentifier>();
var value = r.Read<AtrEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
break;
}
}
return true;
}
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (!data.StartsWith("META0001"u8))
{
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<ImcIdentifier>();
var value = r.Read<ImcEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var eqpCount = r.ReadInt32();
for (var i = 0; i < eqpCount; ++i)
{
var identifier = r.Read<EqpIdentifier>();
var value = r.Read<EqpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var eqdpCount = r.ReadInt32();
for (var i = 0; i < eqdpCount; ++i)
{
var identifier = r.Read<EqdpIdentifier>();
var value = r.Read<EqdpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var estCount = r.ReadInt32();
for (var i = 0; i < estCount; ++i)
{
var identifier = r.Read<EstIdentifier>();
var value = r.Read<EstEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var rspCount = r.ReadInt32();
for (var i = 0; i < rspCount; ++i)
{
var identifier = r.Read<RspIdentifier>();
var value = r.Read<RspEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var gmpCount = r.ReadInt32();
for (var i = 0; i < gmpCount; ++i)
{
var identifier = r.Read<GmpIdentifier>();
var value = r.Read<GmpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var globalEqpCount = r.ReadInt32();
for (var i = 0; i < globalEqpCount; ++i)
{
var manip = r.Read<GlobalEqpManipulation>();
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<AtchIdentifier>();
var value = r.Read<AtchEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
// Shp and Atr was added later
if (r.Position < r.Count)
{
var shpCount = r.ReadInt32();
for (var i = 0; i < shpCount; ++i)
{
var identifier = r.Read<ShpIdentifier>();
var value = r.Read<ShpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var atrCount = r.ReadInt32();
for (var i = 0; i < atrCount; ++i)
{
var identifier = r.Read<AtrIdentifier>();
var value = r.Read<AtrEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
}
}
return true;
}
private static bool ConvertManipsV0(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
var json = Encoding.UTF8.GetString(data);
manips = JsonConvert.DeserializeObject<MetaDictionary>(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)}");
var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
}
}

View file

@ -1,4 +1,4 @@
using OtterGui.Extensions;
using OtterGui;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
@ -63,39 +63,13 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
return new AvailableModSettings(dict);
}
public Dictionary<string, (string[], int)>? 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<string, List<string>>, 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<Guid, (bool, int, Dictionary<string, List<string>>, 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<string, List<string>>, 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);
@ -103,32 +77,17 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (!_collectionManager.Storage.ById(collectionId, out var collection))
return (PenumbraApiEc.CollectionMissing, null);
if (collection.Identity.Id == Guid.Empty)
var settings = collection.Id == Guid.Empty
? null
: ignoreInheritance
? collection.Settings[mod.Index]
: collection[mod.Index].Settings;
if (settings == null)
return (PenumbraApiEc.Success, null);
if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings)
return (PenumbraApiEc.Success, settings);
return (PenumbraApiEc.Success, null);
}
public (PenumbraApiEc, Dictionary<string, (bool, int, Dictionary<string, List<string>>, 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<string, (bool, int, Dictionary<string, List<string>>, 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);
var (enabled, priority, dict) = settings.ConvertToShareable(mod);
return (PenumbraApiEc.Success,
(enabled, priority.Value, dict, collection.Settings[mod.Index] == null));
}
public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit)
@ -201,10 +160,10 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
var setting = mod.Groups[groupIdx].Behaviour switch
var setting = mod.Groups[groupIdx] switch
{
GroupDrawBehaviour.MultiSelection => Setting.Multi(optionIdx),
GroupDrawBehaviour.SingleSelection => Setting.Single(optionIdx),
MultiModGroup => Setting.Multi(optionIdx),
SingleModGroup => Setting.Single(optionIdx),
_ => Setting.Zero,
};
var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
@ -225,9 +184,36 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
var settingSuccess = ConvertModSetting(mod, optionGroupName, optionNames, out var groupIdx, out var setting);
if (settingSuccess is not PenumbraApiEc.Success)
return ApiHelpers.Return(settingSuccess, 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 ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
? PenumbraApiEc.Success
@ -252,38 +238,13 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (bool, int, Dictionary<string, List<string>>, 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.GetActualSettings(mod.Index);
var (settings, parent) = collection[mod.Index];
if (settings is { Enabled: true })
ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection);
ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection);
}
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
@ -293,10 +254,9 @@ 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.Identity.Id, mod?.ModPath.Name ?? string.Empty, inherited);
=> ModSettingChanged?.Invoke(type, collection.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)
{
@ -322,41 +282,4 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
TriggerSettingEdited(mod);
}
public static PenumbraApiEc ConvertModSetting(Mod mod, string groupName, IReadOnlyList<string> 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;
}
}

View file

@ -1,4 +1,3 @@
using Newtonsoft.Json.Linq;
using OtterGui.Compression;
using OtterGui.Services;
using Penumbra.Api.Enums;
@ -16,17 +15,15 @@ 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, MigrationManager migrationManager)
CommunicatorService communicator)
{
_modManager = modManager;
_modImportManager = modImportManager;
_config = config;
_modFileSystem = modFileSystem;
_communicator = communicator;
_migrationManager = migrationManager;
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods);
}
@ -34,8 +31,12 @@ 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;
@ -43,9 +44,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
}
public void Dispose()
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
}
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
public Dictionary<string, string> GetModList()
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
@ -76,22 +75,13 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
if (!dir.Exists)
return ApiHelpers.Return(PenumbraApiEc.FileMissing, args);
if (dir.Parent == null
|| Path.TrimEndingDirectorySeparator(Path.GetFullPath(_modManager.BasePath.FullName))
!= Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
if (_modManager.BasePath.FullName != dir.Parent?.FullName)
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
_modManager.AddMod(dir, true);
if (_config.MigrateImportedModelsToV6)
{
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
_migrationManager.Await();
}
_modManager.AddMod(dir);
if (_config.UseFileSystemCompression)
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
CompressionAlgorithm.Xpress8K, false);
CompressionAlgorithm.Xpress8K);
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}
@ -108,22 +98,10 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
public event Action<string>? ModAdded;
public event Action<string, string>? ModMoved;
public event Action<JObject, ushort, string>? CreatingPcp
{
add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi);
remove => _communicator.PcpCreation.Unsubscribe(value!);
}
public event Action<JObject, string, Guid>? ParsingPcp
{
add => _communicator.PcpParsing.Subscribe(value!, PcpParsing.Priority.ModsApi);
remove => _communicator.PcpParsing.Unsubscribe(value!);
}
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
{
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|| !_modFileSystem.FindLeaf(mod, out var leaf))
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
var fullPath = leaf.FullName();
@ -138,7 +116,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
return PenumbraApiEc.InvalidArgument;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|| !_modFileSystem.FindLeaf(mod, out var leaf))
return PenumbraApiEc.ModMissing;
try
@ -151,15 +129,4 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
return PenumbraApiEc.PathRenameFailed;
}
}
public Dictionary<string, object?> GetChangedItems(string modDirectory, string modName)
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject())
: [];
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>> GetChangedItemAdapterDictionary()
=> new ModChangedItemAdapter(new WeakReference<ModStorage>(_modManager));
public IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)> GetChangedItemAdapterList()
=> new ModChangedItemAdapter(new WeakReference<ModStorage>(_modManager));
}

View file

@ -16,16 +16,13 @@ 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
=> (BreakingVersion, FeatureVersion);
=> (5, 0);
public bool Valid { get; private set; } = true;
public IPenumbraApiCollection Collection { get; } = collection;

View file

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

View file

@ -1,53 +1,23 @@
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, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService
{
public void RedrawObject(int gameObjectIndex, RedrawType setting)
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting));
}
=> redrawService.RedrawObject(gameObjectIndex, setting);
public void RedrawObject(string name, RedrawType setting)
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting));
}
=> redrawService.RedrawObject(name, setting);
public void RedrawObject(IGameObject? gameObject, RedrawType setting)
{
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting));
}
public void RedrawObject(GameObject? gameObject, RedrawType setting)
=> redrawService.RedrawObject(gameObject, setting);
public void RedrawAll(RedrawType 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);
}
}
});
}
=> redrawService.RedrawAll(setting);
public event GameObjectRedrawnDelegate? GameObjectRedrawn
{

View file

@ -1,6 +1,5 @@
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Interop.PathResolving;
@ -42,19 +41,6 @@ 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)
@ -78,26 +64,6 @@ 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)
@ -128,7 +94,7 @@ public class ResolveApi(
if (!config.EnableMods)
return path;
var gamePath = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty;
var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty;
var ret = collection.ResolvePath(gamePath);
return ret?.ToString() ?? path;
}

View file

@ -12,7 +12,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana
{
public Dictionary<string, HashSet<string>>?[] GetGameObjectResourcePaths(params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<ICharacter>();
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<Character>();
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<ICharacter>();
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<Character>();
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<ICharacter>();
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<Character>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);

View file

@ -1,11 +1,10 @@
using OtterGui.Log;
using OtterGui;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
using Penumbra.Mods.Manager;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings;
using Penumbra.String.Classes;
@ -16,20 +15,10 @@ public class TemporaryApi(
ObjectManager objects,
ActorManager actors,
CollectionManager collectionManager,
TempModManager tempMods,
ApiHelpers apiHelpers,
ModManager modManager) : IPenumbraApiTemporary, IApiService
TempModManager tempMods) : IPenumbraApiTemporary, IApiService
{
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 Guid CreateTemporaryCollection(string name)
=> tempCollections.CreateTemporaryCollection(name);
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
=> tempCollections.RemoveTemporaryCollection(collectionId)
@ -73,7 +62,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!MetaApi.ConvertManips(manipString, out var m, out _))
if (!ConvertManips(manipString, out var m))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
@ -99,7 +88,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!MetaApi.ConvertManips(manipString, out var m, out _))
if (!ConvertManips(manipString, out var m))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
@ -138,177 +127,6 @@ public class TemporaryApi(
return ApiHelpers.Return(ret, args);
}
public (PenumbraApiEc, (bool, bool, int, Dictionary<string, List<string>>)?, 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<string, List<string>>)? 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<string, List<string>>)? 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<string, IReadOnlyList<string>> 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<string, IReadOnlyList<string>> 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<string, IReadOnlyList<string>> 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);
}
/// <summary>
/// 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.
@ -319,7 +137,7 @@ public class TemporaryApi(
paths = new Dictionary<Utf8GamePath, FullPath>(redirections.Count);
foreach (var (gString, fString) in redirections)
{
if (!Utf8GamePath.FromString(gString, out var path))
if (!Utf8GamePath.FromString(gString, out var path, false))
{
paths = null;
return false;
@ -335,4 +153,38 @@ public class TemporaryApi(
return true;
}
/// <summary>
/// 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.
/// </summary>
private static bool ConvertManips(string manipString,
[NotNullWhen(true)] out HashSet<MetaManipulation>? manips)
{
if (manipString.Length == 0)
{
manips = [];
return true;
}
if (Functions.FromCompressedBase64<MetaManipulation[]>(manipString, out var manipArray) != MetaManipulation.CurrentVersion)
{
manips = null;
return false;
}
manips = new HashSet<MetaManipulation>(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;
}
}

View file

@ -1,12 +1,10 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Integration;
using Penumbra.UI.Tabs;
namespace Penumbra.Api.Api;
@ -15,14 +13,12 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
private readonly CommunicatorService _communicator;
private readonly ConfigWindow _configWindow;
private readonly ModManager _modManager;
private readonly IntegrationSettingsRegistry _integrationSettings;
public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager, IntegrationSettingsRegistry integrationSettings)
public UiApi(CommunicatorService 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);
}
@ -85,29 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
public void CloseMainWindow()
=> _configWindow.IsOpen = false;
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data)
private void OnChangedItemClick(MouseButton button, object? data)
{
if (ChangedItemClicked == null)
return;
var (type, id) = data.ToApiObject();
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
ChangedItemClicked.Invoke(button, type, id);
}
private void OnChangedItemHover(IIdentifiedObjectData data)
private void OnChangedItemHover(object? data)
{
if (ChangedItemTooltip == null)
return;
var (type, id) = data.ToApiObject();
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
ChangedItemTooltip.Invoke(type, id);
}
public PenumbraApiEc RegisterSettingsSection(Action draw)
=> _integrationSettings.RegisterSection(draw);
public PenumbraApiEc UnregisterSettingsSection(Action draw)
=> _integrationSettings.UnregisterSection(draw)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
}

View file

@ -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,7 +13,6 @@ 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;
@ -22,10 +21,9 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
=> _config.UseDalamudUiTextureRedirection;
public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData,
Configuration config, CommunicatorService communicator, IUiBuilder ui)
Configuration config, CommunicatorService communicator)
{
_substitution = substitution;
_uiBuilder = ui;
_activeCollectionData = activeCollectionData;
_config = config;
_communicator = communicator;
@ -43,9 +41,6 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
public void ResetSubstitutions(IEnumerable<Utf8GamePath> 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());
@ -96,7 +91,10 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
case ResolvedFileChanged.Type.Added:
case ResolvedFileChanged.Type.Removed:
case ResolvedFileChanged.Type.Replaced:
ResetSubstitutions([key]);
ResetSubstitutions(new[]
{
key,
});
break;
case ResolvedFileChanged.Type.FullRecomputeStart:
case ResolvedFileChanged.Type.FullRecomputeFinished:
@ -129,7 +127,7 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService
try
{
if (!Utf8GamePath.FromString(path, out var utf8Path))
if (!Utf8GamePath.FromString(path, out var utf8Path, true))
return;
var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path);

View file

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

View file

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

View file

@ -2,8 +2,6 @@ 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;
@ -13,11 +11,9 @@ public sealed class IpcProviders : IDisposable, IApiService
private readonly EventProvider _disposedProvider;
private readonly EventProvider _initializedProvider;
private readonly CharacterUtility _characterUtility;
public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api, CharacterUtility characterUtility)
public IpcProviders(DalamudPluginInterface pi, IPenumbraApi api)
{
_characterUtility = characterUtility;
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
_providers =
@ -29,7 +25,6 @@ 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),
@ -40,8 +35,6 @@ 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),
@ -54,19 +47,11 @@ 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),
@ -83,13 +68,10 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
@ -99,8 +81,6 @@ 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),
@ -116,14 +96,6 @@ 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),
@ -133,24 +105,12 @@ 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();

View file

@ -1,23 +1,23 @@
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.Data;
using Penumbra.GameData.Enums;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Penumbra.Api.IpcTester;
public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService
{
private int _objectIdx;
private string _collectionIdString = string.Empty;
private Guid? _collectionId;
private Guid? _collectionId = null;
private bool _allowCreation = true;
private bool _allowDeletion = true;
private ApiCollectionType _type = ApiCollectionType.Yourself;
@ -116,15 +116,11 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
_changedItems = items.Select(kvp =>
{
var (type, id) = kvp.Value.ToApiObject();
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value);
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()
@ -134,9 +130,9 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
if (!p)
return;
using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
{
if (table)
if (t)
ImGuiClip.ClippedDraw(_changedItems, t =>
{
ImGuiUtil.DrawTableColumn(t.Item1);

View file

@ -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(IDalamudPluginInterface pi) : IUiService
public class EditingIpcTester(DalamudPluginInterface pi) : IUiService
{
private string _inputPath = string.Empty;
private string _inputPath2 = string.Empty;

View file

@ -1,6 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester;
public class GameStateIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private readonly DalamudPluginInterface _pi;
public readonly EventSubscriber<nint, Guid, nint, nint, nint> CharacterBaseCreating;
public readonly EventSubscriber<nint, Guid, nint> CharacterBaseCreated;
public readonly EventSubscriber<nint, string, string> GameObjectResourcePathResolved;
@ -30,7 +30,7 @@ public class GameStateIpcTester : IUiService, IDisposable
private int _currentCutsceneParent;
private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success;
public GameStateIpcTester(IDalamudPluginInterface pi)
public GameStateIpcTester(DalamudPluginInterface pi)
{
_pi = pi;
CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated);
@ -134,6 +134,7 @@ public class GameStateIpcTester : IUiService, IDisposable
private static unsafe string GetObjectName(nint gameObject)
{
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject;
return obj != null && obj->Name[0] != 0 ? new ByteString(obj->Name).ToString() : "Unknown";
var name = obj != null ? obj->Name : null;
return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown";
}
}

View file

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

View file

@ -1,20 +1,14 @@
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(IDalamudPluginInterface pi) : IUiService
public class MetaIpcTester(DalamudPluginInterface pi) : IUiService
{
private int _gameObjectIndex;
private string _metaBase64 = string.Empty;
private MetaDictionary _metaDict = new();
private byte _parsedVersion = byte.MaxValue;
public void Draw()
{
@ -23,11 +17,6 @@ public class MetaIpcTester(IDalamudPluginInterface 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;
@ -45,8 +34,5 @@ public class MetaIpcTester(IDalamudPluginInterface 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}");
}
}

View file

@ -1,9 +1,8 @@
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;
@ -13,7 +12,7 @@ namespace Penumbra.Api.IpcTester;
public class ModSettingsIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private readonly DalamudPluginInterface _pi;
public readonly EventSubscriber<ModSettingChange, Guid, string, bool> SettingChanged;
private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
@ -28,17 +27,13 @@ public class ModSettingsIpcTester : IUiService, IDisposable
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<string, (string[], GroupType)>? _availableSettings;
private Dictionary<string, List<string>>? _currentSettings;
private Dictionary<string, (bool, int, Dictionary<string, List<string>>, bool, bool)>? _allSettings;
public ModSettingsIpcTester(IDalamudPluginInterface pi)
public ModSettingsIpcTester(DalamudPluginInterface pi)
{
_pi = pi;
SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting);
@ -59,9 +54,7 @@ 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);
ImUtf8.Checkbox("Ignore Inheritance"u8, ref _settingsIgnoreInheritance);
ImUtf8.Checkbox("Ignore Temporary"u8, ref _settingsIgnoreTemporary);
ImUtf8.InputScalar("Key"u8, ref _settingsKey);
ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance);
var collection = _settingsCollection.GetValueOrDefault(Guid.Empty);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
@ -92,7 +85,6 @@ public class ModSettingsIpcTester : IUiService, IDisposable
{
_settingsEnabled = ret.Item2?.Item1 ?? false;
_settingsInherit = ret.Item2?.Item4 ?? true;
_settingsTemporary = false;
_settingsPriority = ret.Item2?.Item2 ?? 0;
_currentSettings = ret.Item2?.Item3;
}
@ -102,40 +94,6 @@ 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();

View file

@ -1,9 +1,8 @@
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;
@ -12,7 +11,7 @@ namespace Penumbra.Api.IpcTester;
public class ModsIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private readonly DalamudPluginInterface _pi;
private string _modDirectory = string.Empty;
private string _modName = string.Empty;
@ -24,7 +23,6 @@ public class ModsIpcTester : IUiService, IDisposable
private PenumbraApiEc _lastSetPathEc;
private PenumbraApiEc _lastInstallEc;
private Dictionary<string, string> _mods = [];
private Dictionary<string, object?> _changedItems = [];
public readonly EventSubscriber<string> DeleteSubscriber;
public readonly EventSubscriber<string> AddSubscriber;
@ -38,7 +36,7 @@ public class ModsIpcTester : IUiService, IDisposable
private string _lastMovedModFrom = string.Empty;
private string _lastMovedModTo = string.Empty;
public ModsIpcTester(IDalamudPluginInterface pi)
public ModsIpcTester(DalamudPluginInterface pi)
{
_pi = pi;
DeleteSubscriber = ModDeleted.Subscriber(pi, s =>
@ -122,14 +120,6 @@ 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}]");
@ -167,18 +157,4 @@ 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();
}
}

View file

@ -1,11 +1,10 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers;
@ -13,7 +12,7 @@ namespace Penumbra.Api.IpcTester;
public class PluginStateIpcTester : IUiService, IDisposable
{
private readonly IDalamudPluginInterface _pi;
private readonly DalamudPluginInterface _pi;
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
public readonly EventSubscriber Initialized;
public readonly EventSubscriber Disposed;
@ -27,13 +26,10 @@ public class PluginStateIpcTester : IUiService, IDisposable
private readonly List<DateTimeOffset> _initializedList = [];
private readonly List<DateTimeOffset> _disposedList = [];
private string _requiredFeatureString = string.Empty;
private string[] _requiredFeatures = [];
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
private bool? _lastEnabledValue;
public PluginStateIpcTester(IDalamudPluginInterface pi)
public PluginStateIpcTester(DalamudPluginInterface pi)
{
_pi = pi;
ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged);
@ -52,15 +48,12 @@ 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;
@ -78,12 +71,6 @@ public class PluginStateIpcTester : IUiService, IDisposable
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
IpcTester.DrawIntro(SupportedFeatures.Label, "Supported Features");
ImUtf8.Text(string.Join(", ", new SupportedFeatures(_pi).Invoke()));
IpcTester.DrawIntro(CheckSupportedFeatures.Label, "Missing Features");
ImUtf8.Text(string.Join(", ", new CheckSupportedFeatures(_pi).Invoke(_requiredFeatures)));
DrawConfigPopup();
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
if (ImGui.Button("Get"))

View file

@ -1,6 +1,6 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Bindings.ImGui;
using ImGuiNET;
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 IDalamudPluginInterface _pi;
private readonly DalamudPluginInterface _pi;
private readonly ObjectManager _objects;
public readonly EventSubscriber<nint, int> Redrawn;
private int _redrawIndex;
private string _lastRedrawnString = "None";
public RedrawingIpcTester(IDalamudPluginInterface pi, ObjectManager objects)
public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects)
{
_pi = pi;
_objects = objects;

View file

@ -1,5 +1,5 @@
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using ImGuiNET;
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(IDalamudPluginInterface pi) : IUiService
public class ResolveIpcTester(DalamudPluginInterface pi) : IUiService
{
private string _currentResolvePath = string.Empty;
private string _currentReversePath = string.Empty;

View file

@ -1,10 +1,9 @@
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;
@ -16,7 +15,7 @@ using Penumbra.GameData.Structs;
namespace Penumbra.Api.IpcTester;
public class ResourceTreeIpcTester(IDalamudPluginInterface pi, ObjectManager objects) : IUiService
public class ResourceTreeIpcTester(DalamudPluginInterface pi, ObjectManager objects) : IUiService
{
private readonly Stopwatch _stopwatch = new();

View file

@ -1,15 +1,13 @@
using Dalamud.Interface;
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using ImGuiNET;
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;
@ -17,7 +15,7 @@ using Penumbra.Services;
namespace Penumbra.Api.IpcTester;
public class TemporaryIpcTester(
IDalamudPluginInterface pi,
DalamudPluginInterface pi,
ModManager modManager,
CollectionManager collections,
TempModManager tempMods,
@ -28,17 +26,13 @@ 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;
@ -49,15 +43,13 @@ 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("##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.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256);
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
@ -75,7 +67,7 @@ public class TemporaryIpcTester(
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
if (ImGui.Button("Create##Collection"))
{
_lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName);
if (_tempGuid == null)
{
_tempGuid = LastCreatedCollectionId;
@ -109,7 +101,8 @@ public class TemporaryIpcTester(
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
var manips = MetaApi.CompressMetaManipulations(copyCollection);
var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>(),
MetaManipulation.CurrentVersion);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
}
@ -126,124 +119,15 @@ 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<string, IReadOnlyList<string>>(),
"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<string, IReadOnlyList<string>>(),
"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<string, List<string>> 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 = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8);
using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
if (!collTree)
return;
using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit);
using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
@ -254,16 +138,16 @@ public class TemporaryIpcTester(
var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
.FirstOrDefault()
?? "Unknown";
if (_debug && ImUtf8.Button("Save##Collection"u8))
if (ImGui.Button("Save##Collection"))
TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(collection.Identity.Identifier);
ImGuiUtil.CopyOnClickSelectable(collection.Identifier);
}
ImGuiUtil.DrawTableColumn(collection.Identity.Name);
ImGuiUtil.DrawTableColumn(collection.Name);
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
ImGuiUtil.DrawTableColumn(string.Join(", ",
@ -284,7 +168,7 @@ public class TemporaryIpcTester(
foreach (var mod in list)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Name.Text);
ImGui.TextUnformatted(mod.Name);
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Priority.ToString());
ImGui.TableNextColumn();
@ -303,8 +187,8 @@ public class TemporaryIpcTester(
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
foreach (var identifier in mod.Default.Manipulations.Identifiers)
ImGui.TextUnformatted(identifier.ToString());
foreach (var manip in mod.Default.Manipulations)
ImGui.TextUnformatted(manip.ToString());
}
}
}
@ -313,7 +197,7 @@ public class TemporaryIpcTester(
{
PrintList("All", tempMods.ModsForAllCollections);
foreach (var (collection, list) in tempMods.Mods)
PrintList(collection.Identity.Name, list);
PrintList(collection.Name, list);
}
}
}

View file

@ -1,5 +1,5 @@
using Dalamud.Plugin;
using Dalamud.Bindings.ImGui;
using ImGuiNET;
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 IDalamudPluginInterface _pi;
private readonly DalamudPluginInterface _pi;
public readonly EventSubscriber<string, float, float> PreSettingsTabBar;
public readonly EventSubscriber<string> PreSettingsPanel;
public readonly EventSubscriber<string> PostEnabled;
@ -28,7 +28,7 @@ public class UiIpcTester : IUiService, IDisposable
private string _modName = string.Empty;
private PenumbraApiEc _ec = PenumbraApiEc.Success;
public UiIpcTester(IDalamudPluginInterface pi)
public UiIpcTester(DalamudPluginInterface pi)
{
_pi = pi;
PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod);

View file

@ -1,103 +0,0 @@
using Penumbra.GameData.Data;
using Penumbra.Mods.Manager;
namespace Penumbra.Api;
public sealed class ModChangedItemAdapter(WeakReference<ModStorage> storage)
: IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>,
IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>
{
IEnumerator<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>
IEnumerable<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>.GetEnumerator()
=> Storage.Select(m => (m.Identifier, (IReadOnlyDictionary<string, object?>)new ChangedItemDictionaryAdapter(m.ChangedItems)))
.GetEnumerator();
public IEnumerator<KeyValuePair<string, IReadOnlyDictionary<string, object?>>> GetEnumerator()
=> Storage.Select(m => new KeyValuePair<string, IReadOnlyDictionary<string, object?>>(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<string, object?>? value)
{
if (Storage.TryGetMod(key, string.Empty, out var mod))
{
value = new ChangedItemDictionaryAdapter(mod.ChangedItems);
return true;
}
value = null;
return false;
}
public IReadOnlyDictionary<string, object?> this[string key]
=> TryGetValue(key, out var v) ? v : throw new KeyNotFoundException();
(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)
IReadOnlyList<(string ModDirectory, IReadOnlyDictionary<string, object?> ChangedItems)>.this[int index]
{
get
{
var m = Storage[index];
return (m.Identifier, new ChangedItemDictionaryAdapter(m.ChangedItems));
}
}
public IEnumerable<string> Keys
=> Storage.Select(m => m.Identifier);
public IEnumerable<IReadOnlyDictionary<string, object?>> 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<string, IIdentifiedObjectData> data) : IReadOnlyDictionary<string, object?>
{
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
=> data.Select(d => new KeyValuePair<string, object?>(d.Key, d.Value?.ToInternalObject())).GetEnumerator();
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<string> Keys
=> data.Keys;
public IEnumerable<object?> Values
=> data.Values.Select(v => v?.ToInternalObject());
}
}

View file

@ -1,4 +1,3 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
@ -19,7 +18,7 @@ public enum RedirectResult
FilteredGamePath = 3,
}
public class TempModManager : IDisposable, IService
public class TempModManager : IDisposable
{
private readonly CommunicatorService _communicator;
@ -44,7 +43,7 @@ public class TempModManager : IDisposable, IService
=> _modsForAllCollections;
public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
MetaDictionary manips, ModPriority priority)
HashSet<MetaManipulation> manips, ModPriority priority)
{
var mod = GetOrCreateMod(tag, collection, priority, out var created);
Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}.");
@ -85,13 +84,13 @@ public class TempModManager : IDisposable, IService
{
if (removed)
{
Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.Identity.AnonymizedName}.");
Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}.");
collection.Remove(mod);
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false);
}
else
{
Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.Identity.AnonymizedName}.");
Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}.");
collection.Apply(mod, created);
_communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false);
}

View file

@ -1,57 +0,0 @@
using Dalamud.Bindings.ImGui;
using OtterGui.Text;
namespace Penumbra;
public enum ChangedItemMode
{
GroupedCollapsed,
GroupedExpanded,
Alphabetical,
}
public static class ChangedItemModeExtensions
{
public static ReadOnlySpan<byte> ToName(this ChangedItemMode mode)
=> mode switch
{
ChangedItemMode.GroupedCollapsed => "Grouped (Collapsed)"u8,
ChangedItemMode.GroupedExpanded => "Grouped (Expanded)"u8,
ChangedItemMode.Alphabetical => "Alphabetical"u8,
_ => "Error"u8,
};
public static ReadOnlySpan<byte> ToTooltip(this ChangedItemMode mode)
=> mode switch
{
ChangedItemMode.GroupedCollapsed =>
"Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item."u8,
ChangedItemMode.GroupedExpanded =>
"Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item."u8,
ChangedItemMode.Alphabetical => "Display all changed items in a single list sorted alphabetically."u8,
_ => ""u8,
};
public static bool DrawCombo(ReadOnlySpan<byte> label, ChangedItemMode value, float width, Action<ChangedItemMode> setter)
{
ImGui.SetNextItemWidth(width);
using var combo = ImUtf8.Combo(label, value.ToName());
if (!combo)
return false;
var ret = false;
foreach (var newValue in Enum.GetValues<ChangedItemMode>())
{
var selected = ImUtf8.Selectable(newValue.ToName(), newValue == value);
if (selected)
{
ret = true;
setter(newValue);
}
ImUtf8.HoverTooltip(newValue.ToTooltip());
}
return ret;
}
}

View file

@ -1,121 +0,0 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class AtchCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtchIdentifier, AtchEntry>(manager, collection)
{
private readonly Dictionary<GenderRace, (AtchFile, HashSet<AtchIdentifier>)> _atchFiles = [];
public bool HasFile(GenderRace gr)
=> _atchFiles.ContainsKey(gr);
public bool GetFile(GenderRace gr, [NotNullWhen(true)] out AtchFile? file)
{
if (!_atchFiles.TryGetValue(gr, out var p))
{
file = null;
return false;
}
file = p.Item1;
return true;
}
public void Reset()
{
foreach (var (_, (_, set)) in _atchFiles)
set.Clear();
_atchFiles.Clear();
Clear();
}
protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry)
{
Collection.Counters.IncrementAtch();
ApplyFile(identifier, entry);
}
private void ApplyFile(AtchIdentifier identifier, AtchEntry entry)
{
try
{
if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair))
{
if (!Manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile))
throw new Exception($"Invalid Atch File for {identifier.GenderRace.ToName()} requested.");
pair = (baseFile.Clone(), []);
}
if (!Apply(pair.Item1, identifier, entry))
return;
pair.Item2.Add(identifier);
_atchFiles[identifier.GenderRace] = pair;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not apply ATCH Manipulation {identifier}:\n{e}");
}
}
protected override void RevertModInternal(AtchIdentifier identifier)
{
Collection.Counters.IncrementAtch();
if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair))
return;
if (!pair.Item2.Remove(identifier))
return;
if (pair.Item2.Count == 0)
{
_atchFiles.Remove(identifier.GenderRace);
return;
}
var def = GetDefault(Manager, identifier);
if (def == null)
throw new Exception($"Reverting an .atch mod had no default value for the identifier to revert to.");
Apply(pair.Item1, identifier, def.Value);
}
public static AtchEntry? GetDefault(MetaFileManager manager, AtchIdentifier identifier)
{
if (!manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile))
return null;
if (baseFile.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point)
return null;
if (point.Entries.Length <= identifier.EntryIndex)
return null;
return point.Entries[identifier.EntryIndex];
}
public static bool Apply(AtchFile file, AtchIdentifier identifier, in AtchEntry entry)
{
if (file.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point)
return false;
if (point.Entries.Length <= identifier.EntryIndex)
return false;
point.Entries[identifier.EntryIndex] = entry;
return true;
}
protected override void Dispose(bool _)
{
Clear();
_atchFiles.Clear();
}
}

View file

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

View file

@ -0,0 +1,56 @@
using OtterGui.Filesystem;
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 CmpCache : IDisposable
{
private CmpFile? _cmpFile = null;
private readonly List<RspManipulation> _cmpManipulations = new();
public CmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public void Reset()
{
if (_cmpFile == null)
return;
_cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute)));
_cmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, RspManipulation manip)
{
_cmpManipulations.AddOrReplace(manip);
_cmpFile ??= new CmpFile(manager);
return manip.Apply(_cmpFile);
}
public bool RevertMod(MetaFileManager manager, RspManipulation manip)
{
if (!_cmpManipulations.Remove(manip))
return false;
var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute);
manip = new RspManipulation(manip.SubRace, manip.Attribute, def);
return manip.Apply(_cmpFile!);
}
public void Dispose()
{
_cmpFile?.Dispose();
_cmpFile = null;
_cmpManipulations.Clear();
}
}

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui;
using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
@ -6,8 +6,6 @@ using Penumbra.Communication;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.Util;
using Penumbra.GameData.Data;
using OtterGui.Extensions;
namespace Penumbra.Collections.Cache;
@ -23,7 +21,7 @@ public sealed class CollectionCache : IDisposable
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> _changedItems = [];
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta;
@ -32,7 +30,7 @@ public sealed class CollectionCache : IDisposable
public int Calculating = -1;
public string AnonymizedName
=> _collection.Identity.AnonymizedName;
=> _collection.AnonymizedName;
public IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> ConflictDict.Values;
@ -43,7 +41,7 @@ public sealed class CollectionCache : IDisposable
private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
public IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
{
get
{
@ -127,6 +125,12 @@ 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));
@ -178,7 +182,7 @@ public sealed class CollectionCache : IDisposable
var (paths, manipulations) = ModData.RemoveMod(mod);
if (addMetaChanges)
_collection.Counters.IncrementChange();
_collection.IncrementCounter();
foreach (var path in paths)
{
@ -229,33 +233,15 @@ public sealed class CollectionCache : IDisposable
foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, 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!);
}
foreach (var manip in files.Manipulations)
AddManipulation(manip, mod);
if (addMetaChanges)
{
_collection.Counters.IncrementChange();
_collection.IncrementCounter();
if (mod.TotalManipulations > 0)
AddMetaFiles(false);
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
@ -265,7 +251,7 @@ public sealed class CollectionCache : IDisposable
if (mod.Index < 0)
return mod.GetData();
var settings = _collection.GetActualSettings(mod.Index).Settings;
var settings = _collection[mod.Index].Settings;
return settings is not { Enabled: true }
? AppliedModData.Empty
: mod.GetData(settings);
@ -279,24 +265,6 @@ 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.
@ -306,9 +274,6 @@ public sealed class CollectionCache : IDisposable
if (!CheckFullPath(path, file))
return;
if (!IsRedirectionSupported(path, mod))
return;
try
{
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
@ -368,9 +333,8 @@ 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.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority;
var existingPriority =
existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority;
var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority;
var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority;
if (existingPriority < addedPriority)
{
@ -378,7 +342,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 IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0)
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0)
AddConflict(data, addedMod, conflict.Mod2);
}
@ -410,12 +374,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(IMod mod, IMetaIdentifier identifier, object entry)
private void AddManipulation(MetaManipulation manip, IMod mod)
{
if (!Meta.TryGetMod(identifier, out var existingMod))
if (!Meta.TryGetValue(manip, out var existingMod))
{
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
return;
}
@ -423,29 +387,34 @@ public sealed class CollectionCache : IDisposable
if (mod == existingMod)
return;
if (AddConflict(identifier, mod, existingMod))
if (AddConflict(manip, mod, existingMod))
{
ModData.RemoveManip(existingMod, identifier);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
ModData.RemoveManip(existingMod, manip);
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
}
}
// 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.Counters.Change)
if (_changedItemsSaveCounter == _collection.ChangeCounter)
return;
try
{
_changedItemsSaveCounter = _collection.Counters.Change;
_changedItemsSaveCounter = _collection.ChangeCounter;
_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<string, IIdentifiedObjectData>(512);
var items = new SortedList<string, object?>(512);
void AddItems(IMod mod)
{
@ -454,9 +423,8 @@ public sealed class CollectionCache : IDisposable
if (!_changedItems.TryGetValue(name, out var data))
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
else if (!data.Item1.Contains(mod))
_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.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, x + y);
}
@ -469,9 +437,9 @@ public sealed class CollectionCache : IDisposable
AddItems(modPath.Mod);
}
foreach (var (manip, mod) in Meta.IdentifierSources)
foreach (var (manip, mod) in Meta)
{
manip.AddChangedItems(identifier, items);
identifier.MetaChangedItems(items, manip);
AddItems(mod);
}

View file

@ -1,11 +1,10 @@
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.Hooks.ResourceLoading;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
@ -18,7 +17,7 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class CollectionCacheManager : IDisposable, IService
public class CollectionCacheManager : IDisposable
{
private readonly FrameworkManager _framework;
private readonly CommunicatorService _communicator;
@ -71,7 +70,7 @@ public class CollectionCacheManager : IDisposable, IService
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager);
if (!MetaFileManager.CharacterUtility.Ready)
MetaFileManager.CharacterUtility.LoadingFinished.Subscribe(IncrementCounters, CharacterUtilityFinished.Priority.CollectionCacheManager);
MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters;
}
public void Dispose()
@ -83,13 +82,7 @@ public class CollectionCacheManager : IDisposable, IService
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange);
MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
foreach (var collection in _storage)
{
collection._cache?.Dispose();
collection._cache = null;
}
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
}
public void AddChange(CollectionCache.ChangeData data)
@ -114,16 +107,16 @@ public class CollectionCacheManager : IDisposable, IService
/// <summary> Only creates a new cache, does not update an existing one. </summary>
public bool CreateCache(ModCollection collection)
{
if (collection.Identity.Index == ModCollection.Empty.Identity.Index)
if (collection.Index == ModCollection.Empty.Index)
return false;
if (collection._cache != null)
return false;
collection._cache = new CollectionCache(this, collection);
if (collection.Identity.Index > 0)
if (collection.Index > 0)
Interlocked.Increment(ref _count);
Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}.");
Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}.");
return true;
}
@ -132,32 +125,32 @@ public class CollectionCacheManager : IDisposable, IService
/// Does not create caches.
/// </summary>
public void CalculateEffectiveFileList(ModCollection collection)
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identity.Identifier,
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier,
() => CalculateEffectiveFileListInternal(collection));
private void CalculateEffectiveFileListInternal(ModCollection collection)
{
// Skip the empty collection.
if (collection.Identity.Index == 0)
if (collection.Index == 0)
return;
Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName}");
Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}");
if (!collection.HasCache)
{
Penumbra.Log.Error(
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists.");
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists.");
}
else if (collection._cache!.Calculating != -1)
{
Penumbra.Log.Error(
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
}
else
{
FullRecalculation(collection);
Penumbra.Log.Debug(
$"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.Identity.AnonymizedName} finished.");
$"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished.");
}
}
@ -171,7 +164,8 @@ public class CollectionCacheManager : IDisposable, IService
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();
@ -186,7 +180,9 @@ public class CollectionCacheManager : IDisposable, IService
foreach (var mod in _modStorage)
cache.AddModSync(mod, false);
collection.Counters.IncrementChange();
cache.AddMetaFiles(true);
collection.IncrementCounter();
MetaFileManager.ApplyDefaultFiles(collection);
ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty,
@ -212,7 +208,7 @@ public class CollectionCacheManager : IDisposable, IService
else
{
RemoveCache(old);
if (type is not CollectionType.Inactive && newCollection != null && newCollection.Identity.Index != 0 && CreateCache(newCollection))
if (type is not CollectionType.Inactive && newCollection != null && newCollection.Index != 0 && CreateCache(newCollection))
CalculateEffectiveFileList(newCollection);
if (type is CollectionType.Default)
@ -230,11 +226,11 @@ public class CollectionCacheManager : IDisposable, IService
{
case ModPathChangeType.Deleted:
case ModPathChangeType.StartingReload:
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.RemoveMod(mod, true);
break;
case ModPathChangeType.Moved:
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.ReloadMod(mod, true);
break;
}
@ -245,7 +241,7 @@ public class CollectionCacheManager : IDisposable, IService
if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded))
return;
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true))
foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.AddMod(mod, true);
}
@ -257,12 +253,12 @@ public class CollectionCacheManager : IDisposable, IService
private void RemoveCache(ModCollection? collection)
{
if (collection != null
&& 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))
&& 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))
ClearCache(collection);
}
@ -272,7 +268,7 @@ public class CollectionCacheManager : IDisposable, IService
{
if (type is ModOptionChangeType.PrepareChange)
{
foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true }))
collection._cache!.RemoveMod(mod, false);
return;
@ -283,7 +279,7 @@ public class CollectionCacheManager : IDisposable, IService
if (!recomputeList)
return;
foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true }))
foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true }))
{
if (justAdd)
collection._cache!.AddMod(mod, true);
@ -296,8 +292,8 @@ public class CollectionCacheManager : IDisposable, IService
private void IncrementCounters()
{
foreach (var collection in _storage.Where(c => c.HasCache))
collection.Counters.IncrementChange();
MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters);
collection.IncrementCounter();
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _)
@ -316,7 +312,7 @@ public class CollectionCacheManager : IDisposable, IService
cache.AddMod(mod!, true);
else if (oldValue == Setting.True)
cache.RemoveMod(mod!, true);
else if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
else if (collection[mod!.Index].Settings?.Enabled == true)
cache.ReloadMod(mod!, true);
else
cache.RemoveMod(mod!, true);
@ -328,12 +324,9 @@ public class CollectionCacheManager : IDisposable, IService
break;
case ModSettingChange.Setting:
if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true)
cache.ReloadMod(mod, true);
break;
case ModSettingChange.TemporarySetting:
if (collection[mod!.Index].Settings?.Enabled == true)
cache.ReloadMod(mod!, true);
break;
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
@ -361,9 +354,9 @@ public class CollectionCacheManager : IDisposable, IService
collection._cache!.Dispose();
collection._cache = null;
if (collection.Identity.Index > 0)
if (collection.Index > 0)
Interlocked.Decrement(ref _count);
Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}.");
Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}.");
}
/// <summary>

View file

@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache;
/// </summary>
public class CollectionModData
{
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<IMetaIdentifier>)> _data = new();
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<MetaManipulation>)> _data = new();
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<IMetaIdentifier>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<IMetaIdentifier>)kvp.Value.Item2));
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<MetaManipulation>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<MetaManipulation>)kvp.Value.Item2));
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<IMetaIdentifier> Manipulations) RemoveMod(IMod mod)
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<MetaManipulation> 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, IMetaIdentifier manipulation)
public void AddManip(IMod mod, MetaManipulation manipulation)
{
if (_data.TryGetValue(mod, out var data))
{
@ -54,7 +54,7 @@ public class CollectionModData
_data.Remove(mod);
}
public void RemoveManip(IMod mod, IMetaIdentifier manip)
public void RemoveManip(IMod mod, MetaManipulation manip)
{
if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
_data.Remove(mod);

View file

@ -1,6 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.SafeHandles;
using Penumbra.String.Classes;

View file

@ -1,54 +1,97 @@
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 sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqdpIdentifier, EqdpEntry>(manager, collection)
public readonly struct EqdpCache : IDisposable
{
private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries =
[];
private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
private readonly List<EqdpManipulation> _eqdpManipulations = new();
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 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 void Reset()
{
Clear();
_fullEntries.Clear();
}
protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry)
foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>())
{
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;
var relevant = CharacterUtility.RelevantIndices[file.Index.Value];
file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId));
}
protected override void RevertModInternal(EqdpIdentifier identifier)
_eqdpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
{
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
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);
_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);
}
protected override void Dispose(bool _)
public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
{
Clear();
_fullEntries.Clear();
if (!_eqdpManipulations.Remove(manip))
return false;
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);
}
public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory)
=> _eqdpFiles
[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
public void Dispose()
{
for (var i = 0; i < _eqdpFiles.Length; ++i)
{
_eqdpFiles[i]?.Dispose();
_eqdpFiles[i] = null;
}
_eqdpManipulations.Clear();
}
}

View file

@ -1,66 +1,60 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqpIdentifier, EqpEntry>(manager, collection)
public struct EqpCache : IDisposable
{
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);
private ExpandedEqpFile? _eqpFile = null;
private readonly List<EqpManipulation> _eqpManipulations = new();
var combined = bodyEntry | headEntry | handEntry | legsEntry | footEntry;
return PostProcessFeet(PostProcessHands(combined));
}
public EqpCache()
{ }
[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 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);
public void Reset()
=> 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;
if (_eqpFile == null)
return;
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);
_eqpFile.Reset(_eqpManipulations.Select(m => m.SetId));
_eqpManipulations.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static EqpEntry PostProcessFeet(EqpEntry entry)
public bool ApplyMod(MetaFileManager manager, EqpManipulation manip)
{
if (!entry.HasFlag(EqpEntry.FeetHideCalf))
return entry;
_eqpManipulations.AddOrReplace(manip);
_eqpFile ??= new ExpandedEqpFile(manager);
return manip.Apply(_eqpFile);
}
if (entry.HasFlag(EqpEntry.FeetHideKnee) || !entry.HasFlag(EqpEntry._20))
return entry & ~(EqpEntry.LegsHideBootsS | EqpEntry.LegsHideBootsM);
public bool RevertMod(MetaFileManager manager, EqpManipulation manip)
{
var idx = _eqpManipulations.FindIndex(manip.Equals);
if (idx < 0)
return false;
return (entry | EqpEntry.LegsHideBootsM) & ~EqpEntry.LegsHideBootsS;
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();
}
}

View file

@ -1,19 +1,138 @@
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 sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EstIdentifier, EstEntry>(manager, collection)
public struct EstCache : IDisposable
{
public EstEntry GetEstEntry(EstIdentifier identifier)
=> TryGetValue(identifier, out var entry)
? entry.Entry
: EstFile.GetDefault(Manager, identifier);
private EstFile? _estFaceFile = null;
private EstFile? _estHairFile = null;
private EstFile? _estBodyFile = null;
private EstFile? _estHeadFile = null;
private readonly List<EstManipulation> _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 void Reset()
=> Clear();
protected override void Dispose(bool _)
=> Clear();
{
_estFaceFile?.Reset();
_estHairFile?.Reset();
_estBodyFile?.Reset();
_estHeadFile?.Reset();
_estManipulations.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();
}
}

View file

@ -1,14 +1,56 @@
using Penumbra.GameData.Structs;
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<GmpIdentifier, GmpEntry>(manager, collection)
public struct GmpCache : IDisposable
{
public void Reset()
=> Clear();
private ExpandedGmpFile? _gmpFile = null;
private readonly List<GmpManipulation> _gmpManipulations = new();
protected override void Dispose(bool _)
=> Clear();
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;
_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();
}
}

View file

@ -1,102 +1,123 @@
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ImcIdentifier, ImcEntry>(manager, collection)
public readonly struct ImcCache : IDisposable
{
private readonly Dictionary<CiByteString, (ImcFile, HashSet<ImcIdentifier>)> _imcFiles = [];
private readonly Dictionary<Utf8GamePath, ImcFile> _imcFiles = new();
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new();
public bool HasFile(CiByteString path)
=> _imcFiles.ContainsKey(path);
public ImcCache()
{ }
public bool GetFile(CiByteString path, [NotNullWhen(true)] out ImcFile? file)
public void SetFiles(ModCollection collection, bool fromFullCompute)
{
if (!_imcFiles.TryGetValue(path, out var p))
{
file = null;
return false;
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));
}
file = p.Item1;
return true;
}
public void Reset()
public void Reset(ModCollection collection)
{
foreach (var (_, (file, set)) in _imcFiles)
foreach (var (path, file) in _imcFiles)
{
collection._cache!.RemovePath(path);
file.Reset();
set.Clear();
}
_imcFiles.Clear();
Clear();
_imcManipulations.Clear();
}
protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry)
public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip)
{
Collection.Counters.IncrementImc();
ApplyFile(identifier, entry);
if (!manip.Validate(true))
return false;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip));
if (idx < 0)
{
idx = _imcManipulations.Count;
_imcManipulations.Add((manip, null!));
}
private void ApplyFile(ImcIdentifier identifier, ImcEntry entry)
{
var path = identifier.GamePath().Path;
var path = manip.GamePath();
try
{
if (!_imcFiles.TryGetValue(path, out var pair))
pair = (new ImcFile(Manager, identifier), []);
if (!_imcFiles.TryGetValue(path, out var file))
file = new ImcFile(manager, manip);
if (!Apply(pair.Item1, identifier, entry))
return;
_imcManipulations[idx] = (manip, file);
if (!manip.Apply(file))
return false;
pair.Item2.Add(identifier);
_imcFiles[path] = pair;
_imcFiles[path] = file;
var fullPath = CreateImcPath(collection, path);
collection._cache!.ForceFile(path, fullPath);
return true;
}
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 {identifier}:\n{e}");
}
Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}");
}
protected override void RevertModInternal(ImcIdentifier identifier)
return false;
}
public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m)
{
Collection.Counters.IncrementImc();
var path = identifier.GamePath().Path;
if (!_imcFiles.TryGetValue(path, out var pair))
return;
if (!m.Validate(false))
return false;
if (!pair.Item2.Remove(identifier))
return;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m));
if (idx < 0)
return false;
if (pair.Item2.Count == 0)
var (_, file) = _imcManipulations[idx];
_imcManipulations.RemoveAt(idx);
if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file)))
{
_imcFiles.Remove(path);
pair.Item1.Dispose();
return;
}
var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _);
Apply(pair.Item1, identifier, def);
}
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)
_imcFiles.Remove(file.Path);
collection._cache!.ForceFile(file.Path, FullPath.Empty);
file.Dispose();
Clear();
return true;
}
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;
}
public void Dispose()
{
foreach (var file in _imcFiles.Values)
file.Dispose();
_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);
}

View file

@ -1,137 +1,246 @@
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(MetaFileManager manager, ModCollection collection)
public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>>
{
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; }
private readonly MetaFileManager _manager;
private readonly ModCollection _collection;
private readonly Dictionary<MetaManipulation, IMod> _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 int Count
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
=> _manipulations.Count;
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 IReadOnlyCollection<MetaManipulation> Manipulations
=> _manipulations.Keys;
public IEnumerator<KeyValuePair<MetaManipulation, IMod>> 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 void Reset()
{
Eqp.Reset();
Eqdp.Reset();
Est.Reset();
Gmp.Reset();
Rsp.Reset();
Imc.Reset();
Atch.Reset();
Shp.Reset();
Atr.Reset();
GlobalEqp.Clear();
_eqpCache.Reset();
_eqdpCache.Reset();
_estCache.Reset();
_gmpCache.Reset();
_cmpCache.Reset();
_imcCache.Reset(_collection);
_manipulations.Clear();
_globalEqpCache.Clear();
}
public void Dispose()
{
if (IsDisposed)
return;
IsDisposed = true;
Eqp.Dispose();
Eqdp.Dispose();
Est.Dispose();
Gmp.Dispose();
Rsp.Dispose();
Imc.Dispose();
Atch.Dispose();
Shp.Dispose();
Atr.Dispose();
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
_eqpCache.Dispose();
_eqdpCache.Dispose();
_estCache.Dispose();
_gmpCache.Dispose();
_cmpCache.Dispose();
_imcCache.Dispose();
_manipulations.Clear();
}
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<T>((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();
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId)
=> Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId));
public bool ApplyMod(MetaManipulation manip, IMod mod)
{
lock (_manipulations)
{
if (_manipulations.ContainsKey(manip))
_manipulations.Remove(manip);
internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
=> Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace));
_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,
};
}
/// <summary> Set a single file. </summary>
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;
}
}
/// <summary> Set the currently relevant IMC files for the collection cache. </summary>
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);
/// <summary> Try to obtain a manipulated IMC file. </summary>
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;
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);
/// <summary> Use this when CharacterUtility becomes ready. </summary>
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.");
}
}

View file

@ -1,47 +0,0 @@
using OtterGui.Classes;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager, ModCollection collection)
: ReadWriteDictionary<TIdentifier, (IMod Source, TEntry Entry)>
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<TEntry>.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)
{ }
}

View file

@ -1,13 +0,0 @@
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<RspIdentifier, RspEntry>(manager, collection)
{
public void Reset()
=> Clear();
protected override void Dispose(bool _)
=> Clear();
}

View file

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

View file

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

View file

@ -1,82 +0,0 @@
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();
}
}

View file

@ -1,28 +0,0 @@
namespace Penumbra.Collections;
public struct CollectionCounters(int changeCounter)
{
/// <summary> Count the number of changes of the effective file list. </summary>
public int Change { get; private set; } = changeCounter;
/// <summary> Count the number of IMC-relevant changes of the effective file list. </summary>
public int Imc { get; private set; }
/// <summary> Count the number of ATCH-relevant changes of the effective file list. </summary>
public int Atch { get; private set; }
/// <summary> Increment the number of changes in the effective file list. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IncrementChange()
=> ++Change;
/// <summary> Increment the number of IMC-relevant changes in the effective file list. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IncrementImc()
=> ++Imc;
/// <summary> Increment the number of ATCH-relevant changes in the effective file list. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IncrementAtch()
=> ++Atch;
}

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Internal.Notifications;
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.Identity.Name}.", NotificationType.Warning);
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning);
dict.Add(player, ModCollection.Empty);
}
else

View file

@ -1,9 +1,8 @@
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Internal.Notifications;
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;
@ -12,7 +11,7 @@ using Penumbra.UI;
namespace Penumbra.Collections.Manager;
public class ActiveCollectionData : IService
public class ActiveCollectionData
{
public ModCollection Current { get; internal set; } = ModCollection.Empty;
public ModCollection Default { get; internal set; } = ModCollection.Empty;
@ -21,7 +20,7 @@ public class ActiveCollectionData : IService
public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
}
public class ActiveCollections : ISavable, IDisposable, IService
public class ActiveCollections : ISavable, IDisposable
{
public const int Version = 2;
@ -219,7 +218,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
_ => null,
};
if (oldCollection == null || collection == oldCollection || collection.Identity.Index >= _storage.Count)
if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count)
return;
switch (collectionType)
@ -262,13 +261,13 @@ public class ActiveCollections : ISavable, IDisposable, IService
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Identity.Id },
{ nameof(Interface), Interface.Identity.Id },
{ nameof(Current), Current.Identity.Id },
{ nameof(Default), Default.Id },
{ nameof(Interface), Interface.Id },
{ nameof(Current), Current.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.Identity.Id);
jObj.Add(type.ToString(), collection.Id);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer);
@ -282,7 +281,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
.Prepend(Interface)
.Prepend(Default)
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
.SelectMany(c => c.Inheritance.FlatHierarchy).Contains(Current);
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
/// <summary> Save if any of the active collections is changed and set new collections to Current. </summary>
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3)
@ -300,7 +299,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (oldCollection == Interface)
SetCollection(ModCollection.Empty, CollectionType.Interface);
if (oldCollection == Current)
SetCollection(Default.Identity.Index > ModCollection.Empty.Identity.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
for (var i = 0; i < SpecialCollections.Length; ++i)
{
@ -325,11 +324,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
{
var configChanged = false;
// Load the default collection. If the name does not exist take the empty collection.
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? ModCollection.Empty.Identity.Name;
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? ModCollection.Empty.Name;
if (!_storage.ByName(defaultName, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@ -340,11 +339,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Identity.Name;
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
if (!_storage.ByName(interfaceName, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@ -355,11 +354,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the current collection.
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? Default.Identity.Name;
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? Default.Name;
if (!_storage.ByName(currentName, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
@ -404,7 +403,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(defaultId, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@ -415,11 +414,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
var interfaceId = jObject[nameof(Interface)]?.ToObject<Guid>() ?? Default.Identity.Id;
var interfaceId = jObject[nameof(Interface)]?.ToObject<Guid>() ?? Default.Id;
if (!_storage.ById(interfaceId, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
$"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@ -430,11 +429,11 @@ public class ActiveCollections : ISavable, IDisposable, IService
}
// Load the current collection.
var currentId = jObject[nameof(Current)]?.ToObject<Guid>() ?? _storage.DefaultNamed.Identity.Id;
var currentId = jObject[nameof(Current)]?.ToObject<Guid>() ?? _storage.DefaultNamed.Id;
if (!_storage.ById(currentId, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
$"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
@ -587,7 +586,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
{
var global = ByType(CollectionType.Individual, _actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
return (global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index
return global?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
: string.Empty;
}
@ -596,12 +595,12 @@ public class ActiveCollections : ISavable, IDisposable, IService
{
var global = ByType(CollectionType.Individual,
_actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
if ((global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index)
if (global?.Index == checkAssignment.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 != null ? unowned.Identity.Index : null) == checkAssignment.Identity.Index
return unowned?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty;
}
@ -617,7 +616,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (maleNpc == null)
{
maleNpc = Default;
if (maleNpc.Identity.Index != checkAssignment.Identity.Index)
if (maleNpc.Index != checkAssignment.Index)
return string.Empty;
collection1 = CollectionType.Default;
@ -626,7 +625,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (femaleNpc == null)
{
femaleNpc = Default;
if (femaleNpc.Identity.Index != checkAssignment.Identity.Index)
if (femaleNpc.Index != checkAssignment.Index)
return string.Empty;
collection2 = CollectionType.Default;
@ -646,7 +645,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (assignment == null)
continue;
if (assignment.Identity.Index == checkAssignment.Identity.Index)
if (assignment.Index == checkAssignment.Index)
return
$"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
}

View file

@ -1,5 +1,4 @@
using OtterGui.Extensions;
using OtterGui.Services;
using OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
@ -8,7 +7,7 @@ using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService
public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage)
{
/// <summary> Enable or disable the mod inheritance of mod idx. </summary>
public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit)
@ -26,12 +25,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
/// </summary>
public bool SetModState(ModCollection collection, Mod mod, bool newValue)
{
var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Enabled ?? false;
var oldValue = collection.Settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false;
if (newValue == oldValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
collection.GetOwnSettings(mod.Index)!.Enabled = newValue;
((List<ModSettings?>)collection.Settings)[mod.Index]!.Enabled = newValue;
InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True,
0);
return true;
@ -55,12 +54,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
var changes = false;
foreach (var mod in mods)
{
var oldValue = collection.GetOwnSettings(mod.Index)?.Enabled;
var oldValue = collection.Settings[mod.Index]?.Enabled;
if (newValue == oldValue)
continue;
FixInheritance(collection, mod, false);
collection.GetOwnSettings(mod.Index)!.Enabled = newValue;
((List<ModSettings?>)collection.Settings)[mod.Index]!.Enabled = newValue;
changes = true;
}
@ -76,64 +75,35 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
/// </summary>
public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue)
{
var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Priority ?? ModPriority.Default;
var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default;
if (newValue == oldValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
collection.GetOwnSettings(mod.Index)!.Priority = newValue;
((List<ModSettings?>)collection.Settings)[mod.Index]!.Priority = newValue;
InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0);
return true;
}
/// <summary>
/// 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.
/// </summary>
public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue)
{
var settings = collection.GetInheritedSettings(mod.Index).Settings?.Settings;
var settings = collection.Settings[mod.Index] != null
? collection.Settings[mod.Index]!.Settings
: collection[mod.Index].Settings?.Settings;
var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings;
if (oldValue == newValue)
return false;
var inheritance = FixInheritance(collection, mod, false);
collection.GetOwnSettings(mod.Index)!.SetValue(mod, groupIdx, newValue);
((List<ModSettings?>)collection.Settings)[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;
}
/// <summary> Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. </summary>
public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName)
{
@ -144,10 +114,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.GetOwnSettings(sourceMod.Index) is { } ownSettings
? new ModSettings.SavedSettings(ownSettings, sourceMod)
? collection.Settings[sourceMod.Index] != null
? new ModSettings.SavedSettings(collection.Settings[sourceMod.Index]!, sourceMod)
: null
: collection.Settings.Unused.TryGetValue(sourceName, out var s)
: collection.UnusedSettings.TryGetValue(sourceName, out var s)
? s
: null;
@ -177,10 +147,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
// or remove any unused settings for the target if they are inheriting.
if (savedSettings != null)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused)[targetName] = savedSettings.Value;
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings)[targetName] = savedSettings.Value;
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
else if (((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(targetName))
else if (((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(targetName))
{
saveService.QueueSave(new ModCollectionSave(modStorage, collection));
}
@ -195,12 +165,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
/// </summary>
private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit)
{
var settings = collection.GetOwnSettings(mod.Index);
var settings = collection.Settings[mod.Index];
if (inherit == (settings == null))
return false;
var settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod);
collection.Settings.Set(mod.Index, settings1);
((List<ModSettings?>)collection.Settings)[mod.Index] =
inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod);
return true;
}
@ -208,10 +178,8 @@ 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)
{
if (type is not ModSettingChange.TemporarySetting)
saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false);
if (type is not ModSettingChange.TemporarySetting)
RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx);
}
@ -219,7 +187,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
foreach (var directInheritor in directParent.Inheritance.DirectlyInheritedBy)
foreach (var directInheritor in directParent.DirectParentOf)
{
switch (type)
{
@ -228,7 +196,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true);
break;
default:
if (directInheritor.GetOwnSettings(mod!.Index) == null)
if (directInheritor.Settings[mod!.Index] == null)
communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true);
break;
}

View file

@ -1,4 +1,3 @@
using OtterGui.Services;
using Penumbra.Collections.Cache;
namespace Penumbra.Collections.Manager;
@ -9,7 +8,7 @@ public class CollectionManager(
InheritanceManager inheritances,
CollectionCacheManager caches,
TempCollectionManager temp,
CollectionEditor editor) : IService
CollectionEditor editor)
{
public readonly CollectionStorage Storage = storage;
public readonly ActiveCollections Active = active;

View file

@ -1,7 +1,6 @@
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
@ -14,67 +13,20 @@ using Penumbra.Services;
namespace Penumbra.Collections.Manager;
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
{
public static readonly LocalCollectionId Zero = new(0);
public static LocalCollectionId operator +(LocalCollectionId left, int right)
=> new(left.Id + right);
}
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
{
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<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> 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);
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections =
[
ModCollection.Empty,
];
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
private readonly Dictionary<LocalCollectionId, ModCollection>
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
public readonly ModCollection DefaultNamed;
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
/// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
@ -92,7 +44,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
{
if (name.Length != 0)
return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
return _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
collection = ModCollection.Empty;
return true;
@ -102,7 +54,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
{
if (id != Guid.Empty)
return _collections.FindFirst(c => c.Identity.Id == id, out collection);
return _collections.FindFirst(c => c.Id == id, out collection);
collection = ModCollection.Empty;
return true;
@ -117,10 +69,6 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
return ByName(identifier, out collection);
}
/// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary>
public ModCollection ByLocalId(LocalCollectionId localId)
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
@ -152,13 +100,12 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
if (name.Length == 0)
return false;
var newCollection = Create(name, _collections.Count, duplicate);
var newCollection = duplicate?.Duplicate(name, _collections.Count)
?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
_collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false);
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
return true;
}
@ -168,48 +115,42 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
/// </summary>
public bool RemoveCollection(ModCollection collection)
{
if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count)
{
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
return false;
}
if (collection.Identity.Index == DefaultNamed.Identity.Index)
if (collection.Index == DefaultNamed.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.Identity.Index);
_collections.RemoveAt(collection.Index);
// Update indices.
for (var i = collection.Identity.Index; i < Count; ++i)
_collections[i].Identity.Index = i;
_collectionsByLocal.Remove(collection.Identity.LocalId);
for (var i = collection.Index; i < Count; ++i)
_collections[i].Index = i;
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true;
}
/// <summary> Remove all settings for not currently-installed mods from the given collection. </summary>
public int CleanUnavailableSettings(ModCollection collection)
public void CleanUnavailableSettings(ModCollection collection)
{
var count = collection.Settings.Unused.Count;
if (count > 0)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear();
var any = collection.UnusedSettings.Count > 0;
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Clear();
if (any)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
return count;
}
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
public void CleanUnavailableSetting(ModCollection collection, string? setting)
{
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(setting))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
@ -239,7 +180,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
continue;
}
var collection = CreateFromData(id, name, version, settings, inheritance);
var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
try
@ -250,13 +191,13 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
$"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.",
NotificationType.Warning);
}
catch (Exception ex)
{
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
$"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}",
NotificationType.Warning);
}
}
@ -277,7 +218,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
$"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.",
NotificationType.Error);
}
@ -295,14 +236,14 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
/// </summary>
private ModCollection SetDefaultNamedCollection()
{
if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
if (ByName(ModCollection.DefaultCollectionName, out var collection))
return collection;
if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
if (AddCollection(ModCollection.DefaultCollectionName, null))
return _collections[^1];
Penumbra.Messager.NotificationMessage(
$"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.",
$"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.",
NotificationType.Error);
return Count > 1 ? _collections[1] : _collections[0];
}
@ -311,7 +252,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
private void OnModDiscoveryStarted()
{
foreach (var collection in this)
collection.Settings.PrepareModDiscovery(_modStorage);
collection.PrepareModDiscovery(_modStorage);
}
/// <summary> Restore all settings in all collections to mods. </summary>
@ -319,7 +260,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
{
// Re-apply all mod settings.
foreach (var collection in this)
collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
collection.ApplyModSettings(_saveService, _modStorage);
}
/// <summary> Add or remove a mod from all collections, or re-save all collections where the mod has settings. </summary>
@ -330,22 +271,21 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
{
case ModPathChangeType.Added:
foreach (var collection in this)
collection.Settings.AddMod(mod);
collection.AddMod(mod);
break;
case ModPathChangeType.Deleted:
foreach (var collection in this)
collection.Settings.RemoveMod(mod);
collection.RemoveMod(mod);
break;
case ModPathChangeType.Moved:
foreach (var collection in this.Where(collection => collection.GetOwnSettings(mod.Index) != null))
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
break;
case ModPathChangeType.Reloaded:
foreach (var collection in this)
{
if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false)
if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(mod.Index, null);
}
break;
@ -353,8 +293,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
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)
@ -362,9 +301,8 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
foreach (var collection in this)
{
if (collection.GetOwnSettings(mod.Index)?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(mod.Index, null);
}
}
@ -376,9 +314,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
foreach (var collection in this)
{
var (settings, _) = collection.GetActualSettings(mod.Index);
var (settings, _) = collection[mod.Index];
if (settings is { Enabled: true })
collection.Counters.IncrementChange();
collection.IncrementCounter();
}
}
}

View file

@ -48,7 +48,8 @@ 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;
@ -57,7 +58,8 @@ 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);
@ -125,7 +127,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
}
}
public bool TryGetCollection(IGameObject? gameObject, out ModCollection? collection)
public bool TryGetCollection(GameObject? 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)

View file

@ -1,5 +1,5 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Internal.Notifications;
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.Identity.Id);
tmp.Add("Collection", collection.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.Identity.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.",
$"Could not migrate {name} ({collection.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.Identity.AnonymizedName}) to Player Identifier.");
Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier.");
else
Penumbra.Messager.NotificationMessage(
$"Could not migrate {shortName} ({collection.Identity.AnonymizedName}), please look through your individual collections.",
$"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.",
NotificationType.Error);
}
else
{
Penumbra.Messager.NotificationMessage(
$"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.",
$"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.",
NotificationType.Error);
}
}

View file

@ -1,10 +1,12 @@
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using OtterGui.Filesystem;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
@ -13,7 +15,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.
/// </summary>
public class InheritanceManager : IDisposable, IService
public class InheritanceManager : IDisposable
{
public enum ValidInheritance
{
@ -62,10 +64,10 @@ public class InheritanceManager : IDisposable, IService
if (ReferenceEquals(potentialParent, potentialInheritor))
return ValidInheritance.Self;
if (potentialInheritor.Inheritance.DirectlyInheritsFrom.Contains(potentialParent))
if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent))
return ValidInheritance.Contained;
if (potentialParent.Inheritance.FlatHierarchy.Any(c => ReferenceEquals(c, potentialInheritor)))
if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor)))
return ValidInheritance.Circle;
return ValidInheritance.Valid;
@ -82,23 +84,25 @@ public class InheritanceManager : IDisposable, IService
/// <summary> Remove an existing inheritance from a collection. </summary>
public void RemoveInheritance(ModCollection inheritor, int idx)
{
var parent = inheritor.Inheritance.RemoveInheritanceAt(inheritor, idx);
var parent = inheritor.DirectlyInheritsFrom[idx];
((List<ModCollection>)inheritor.DirectlyInheritsFrom).RemoveAt(idx);
((List<ModCollection>)parent.DirectParentOf).Remove(inheritor);
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
RecurseInheritanceChanges(inheritor, true);
Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.AnonymizedName} inheritances.");
RecurseInheritanceChanges(inheritor);
Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances.");
}
/// <summary> Order in the inheritance list is relevant. </summary>
public void MoveInheritance(ModCollection inheritor, int from, int to)
{
if (!inheritor.Inheritance.MoveInheritance(inheritor, from, to))
if (!((List<ModCollection>)inheritor.DirectlyInheritsFrom).Move(from, to))
return;
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
RecurseInheritanceChanges(inheritor, true);
Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}.");
RecurseInheritanceChanges(inheritor);
Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}.");
}
/// <inheritdoc cref="AddInheritance(ModCollection, ModCollection)"/>
@ -107,16 +111,16 @@ public class InheritanceManager : IDisposable, IService
if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid)
return false;
inheritor.Inheritance.AddInheritance(inheritor, parent);
((List<ModCollection>)inheritor.DirectlyInheritsFrom).Add(parent);
((List<ModCollection>)parent.DirectParentOf).Add(inheritor);
if (invokeEvent)
{
_saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor));
_communicator.CollectionInheritanceChanged.Invoke(inheritor, false);
RecurseInheritanceChanges(inheritor);
}
RecurseInheritanceChanges(inheritor, invokeEvent);
Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances.");
Penumbra.Log.Debug($"Added {parent.AnonymizedName} to {inheritor.AnonymizedName} inheritances.");
return true;
}
@ -128,11 +132,11 @@ public class InheritanceManager : IDisposable, IService
{
foreach (var collection in _storage)
{
if (collection.Inheritance.ConsumeNames() is not { } byName)
if (collection.InheritanceByName == null)
continue;
var changes = false;
foreach (var subCollectionName in byName)
foreach (var subCollectionName in collection.InheritanceByName)
{
if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection))
{
@ -140,30 +144,26 @@ public class InheritanceManager : IDisposable, IService
continue;
changes = true;
Penumbra.Messager.NotificationMessage(
$"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.",
NotificationType.Warning);
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
}
else if (_storage.ByName(subCollectionName, out subCollection))
{
changes = true;
Penumbra.Log.Information($"Migrating inheritance for {collection.Identity.AnonymizedName} from name to GUID.");
Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID.");
if (AddInheritance(collection, subCollection, false))
continue;
Penumbra.Messager.NotificationMessage(
$"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.",
NotificationType.Warning);
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
}
else
{
Penumbra.Messager.NotificationMessage(
$"Inherited collection {subCollectionName} for {collection.Identity.AnonymizedName} does not exist, it was removed.",
NotificationType.Warning);
$"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", NotificationType.Warning);
changes = true;
}
}
collection.InheritanceByName = null;
if (changes)
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection));
}
@ -176,22 +176,20 @@ public class InheritanceManager : IDisposable, IService
foreach (var c in _storage)
{
var inheritedIdx = c.Inheritance.DirectlyInheritsFrom.IndexOf(old);
var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old);
if (inheritedIdx >= 0)
RemoveInheritance(c, inheritedIdx);
c.Inheritance.RemoveChild(old);
((List<ModCollection>)c.DirectParentOf).Remove(old);
}
}
private void RecurseInheritanceChanges(ModCollection newInheritor, bool invokeEvent)
private void RecurseInheritanceChanges(ModCollection newInheritor)
{
foreach (var inheritor in newInheritor.Inheritance.DirectlyInheritedBy)
foreach (var inheritor in newInheritor.DirectParentOf)
{
ModCollectionInheritance.UpdateFlattenedInheritance(inheritor);
RecurseInheritanceChanges(inheritor, invokeEvent);
if (invokeEvent)
_communicator.CollectionInheritanceChanged.Invoke(inheritor, true);
RecurseInheritanceChanges(inheritor);
}
}
}

View file

@ -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.GetOwnSettings(i)))
collection.Settings.SetAll(i, FullModSettings.Empty);
if (SettingIsDefaultV0(collection.Settings[i]))
((List<ModSettings?>)collection.Settings)[i] = null;
}
foreach (var (key, _) in collection.Settings.Unused.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList())
collection.Settings.RemoveUnused(key);
foreach (var (key, _) in collection.UnusedSettings.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList())
((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(key);
return true;
}

View file

@ -1,5 +1,4 @@
using OtterGui.Extensions;
using OtterGui.Services;
using OtterGui;
using Penumbra.Api;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
@ -9,7 +8,7 @@ using Penumbra.String;
namespace Penumbra.Collections.Manager;
public class TempCollectionManager : IDisposable, IService
public class TempCollectionManager : IDisposable
{
public int GlobalChangeCounter { get; private set; }
public readonly IndividualCollections Collections;
@ -44,7 +43,7 @@ public class TempCollectionManager : IDisposable, IService
=> _customCollections.Values;
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.Values.FindFirst(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase), out collection);
=> _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection);
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(id, out collection);
@ -53,13 +52,13 @@ public class TempCollectionManager : IDisposable, IService
{
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
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))
var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++);
Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}.");
if (_customCollections.TryAdd(collection.Id, collection))
{
// Temporary collection created.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty);
return collection.Identity.Id;
return collection.Id;
}
return Guid.Empty;
@ -73,9 +72,8 @@ public class TempCollectionManager : IDisposable, IService
return false;
}
_storage.Delete(collection);
Penumbra.Log.Debug($"Deleted temporary collection {collection.Identity.Id}.");
GlobalChangeCounter += Math.Max(collection.Counters.Change + 1 - GlobalChangeCounter, 0);
Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}.");
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
for (var i = 0; i < Collections.Count; ++i)
{
if (Collections[i].Collection != collection)
@ -83,7 +81,7 @@ public class TempCollectionManager : IDisposable, IService
// Temporary collection assignment removed.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Identity.Id} from {Collections[i].DisplayName}.");
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}.");
Collections.Delete(i--);
}
@ -96,7 +94,7 @@ public class TempCollectionManager : IDisposable, IService
return false;
// Temporary collection assignment added.
Penumbra.Log.Verbose($"Assigned temporary collection {collection.Identity.AnonymizedName} to {Collections.Last().DisplayName}.");
Penumbra.Log.Verbose($"Assigned temporary collection {collection.AnonymizedName} to {Collections.Last().DisplayName}.");
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName);
return true;
}
@ -127,6 +125,6 @@ public class TempCollectionManager : IDisposable, IService
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Identity.Id);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id);
}
}

View file

@ -1,9 +1,14 @@
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.GameData.Data;
using Penumbra.Interop.Services;
using Penumbra.Mods.Editor;
using Penumbra.GameData.Structs;
namespace Penumbra.Collections;
@ -43,15 +48,74 @@ 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<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, object?)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();
internal SingleArray<ModConflicts> Conflicts(Mod mod)
=> _cache?.Conflicts(mod) ?? new SingleArray<ModConflicts>();
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;
}

View file

@ -1,3 +1,4 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Settings;
@ -12,129 +13,203 @@ 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.
/// </summary>
public partial class ModCollection
{
public const int CurrentVersion = 2;
public const string DefaultCollectionName = "Default";
public const string EmptyCollectionName = "None";
/// <summary>
/// Create the always available Empty Collection that will always sit at index 0,
/// can not be deleted and does never create a cache.
/// </summary>
public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, new ModSettingProvider(),
new ModCollectionInheritance());
public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, 0, 0, CurrentVersion, [], [], []);
public ModCollectionIdentity Identity;
/// <summary> The name of a collection. </summary>
public string Name { get; set; }
public Guid Id { get; }
public string Identifier
=> Id.ToString();
public string ShortIdentifier
=> Identifier[..8];
public override string ToString()
=> Identity.ToString();
=> Name.Length > 0 ? Name : ShortIdentifier;
public readonly ModSettingProvider Settings;
public ModCollectionInheritance Inheritance;
public CollectionCounters Counters;
/// <summary> Get the first two letters of a collection name and its Index (or None if it is the empty collection). </summary>
public string AnonymizedName
=> this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier;
/// <summary> The index of the collection is set and kept up-to-date by the CollectionManager. </summary>
public int Index { get; internal set; }
public ModSettings? GetOwnSettings(Index idx)
/// <summary>
/// Count the number of changes of the effective file list.
/// This is used for material and imc changes.
/// </summary>
public int ChangeCounter { get; private set; }
/// <summary> Increment the number of changes in the effective file list. </summary>
public int IncrementCounter()
=> ++ChangeCounter;
/// <summary>
/// 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.
/// </summary>
public readonly IReadOnlyList<ModSettings?> Settings;
/// <summary> Settings for deleted mods will be kept via the mods identifier (directory name). </summary>
public readonly IReadOnlyDictionary<string, ModSettings.SavedSettings> UnusedSettings;
/// <summary> Inheritances stored before they can be applied. </summary>
public IReadOnlyList<string>? InheritanceByName;
/// <summary> Contains all direct parent collections this collection inherits settings from. </summary>
public readonly IReadOnlyList<ModCollection> DirectlyInheritsFrom;
/// <summary> Contains all direct child collections that inherit from this collection. </summary>
public readonly IReadOnlyList<ModCollection> DirectParentOf = new List<ModCollection>();
/// <summary> All inherited collections in application order without filtering for duplicates. </summary>
public static IEnumerable<ModCollection> InheritedCollections(ModCollection collection)
=> collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection);
/// <summary>
/// Iterate over all collections inherited from in depth-first order.
/// Skip already visited collections to avoid circular dependencies.
/// </summary>
public IEnumerable<ModCollection> GetFlattenedInheritance()
=> InheritedCollections(this).Distinct();
/// <summary>
/// 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.
/// </summary>
public (ModSettings? Settings, ModCollection Collection) this[Index idx]
{
if (Identity.Index <= 0)
return ModSettings.Empty;
return Settings.Settings[idx].Settings;
}
public TemporaryModSettings? GetTempSettings(Index idx)
get
{
if (Identity.Index <= 0)
return null;
return Settings.Settings[idx].TempSettings;
}
public (ModSettings? Settings, ModCollection Collection) GetInheritedSettings(Index idx)
{
if (Identity.Index <= 0)
if (Index <= 0)
return (ModSettings.Empty, this);
foreach (var collection in Inheritance.FlatHierarchy)
foreach (var collection in GetFlattenedInheritance())
{
var settings = collection.Settings.Settings[idx].Settings;
var settings = collection.Settings[idx];
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);
}
/// <summary> Evaluates all settings along the whole inheritance tree. </summary>
public IEnumerable<ModSettings?> ActualSettings
=> Enumerable.Range(0, Settings.Count).Select(i => GetActualSettings(i).Settings);
=> Enumerable.Range(0, Settings.Count).Select(i => this[i].Settings);
/// <summary>
/// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists.
/// </summary>
public ModCollection Duplicate(string name, LocalCollectionId localId, int index)
public ModCollection Duplicate(string name, int index)
{
Debug.Assert(index > 0, "Collection duplicated with non-positive index.");
return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, Settings.Clone(), Inheritance.Clone());
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()));
}
/// <summary> Constructor for reading from files. </summary>
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, ModCollectionIdentity identity, int version,
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index,
Dictionary<string, ModSettings.SavedSettings> allSettings, IReadOnlyList<string> inheritances)
{
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);
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);
ModCollectionMigration.Migrate(saver, mods, version, ret);
return ret;
}
/// <summary> Constructor for temporary collections. </summary>
public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter)
public static ModCollection CreateTemporary(string name, int index, int changeCounter)
{
Debug.Assert(index < 0, "Temporary collection created with non-negative index.");
var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, new ModSettingProvider(),
new ModCollectionInheritance());
var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []);
return ret;
}
/// <summary> Constructor for empty collections. </summary>
public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount)
public static ModCollection CreateEmpty(string name, int index, int modCount)
{
Debug.Assert(index >= 0, "Empty collection created with negative index.");
return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, ModSettingProvider.Empty(modCount),
new ModCollectionInheritance());
return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [],
[]);
}
private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, ModSettingProvider settings,
ModCollectionInheritance inheritance)
/// <summary> Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. </summary>
internal bool AddMod(Mod mod)
{
Identity = identity;
Counters = new CollectionCounters(changeCounter);
Settings = settings;
Inheritance = inheritance;
ModCollectionInheritance.UpdateChildren(this);
ModCollectionInheritance.UpdateFlattenedInheritance(this);
if (UnusedSettings.TryGetValue(mod.ModPath.Name, out var save))
{
var ret = save.ToSettings(mod, out var settings);
((List<ModSettings?>)Settings).Add(settings);
((Dictionary<string, ModSettings.SavedSettings>)UnusedSettings).Remove(mod.ModPath.Name);
return ret;
}
((List<ModSettings?>)Settings).Add(null);
return false;
}
/// <summary> Move settings from the current mod list to the unused mod settings. </summary>
internal void RemoveMod(Mod mod)
{
var settings = Settings[mod.Index];
if (settings != null)
((Dictionary<string, ModSettings.SavedSettings>)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod);
((List<ModSettings?>)Settings).RemoveAt(mod.Index);
}
/// <summary> Move all settings to unused settings for rediscovery. </summary>
internal void PrepareModDiscovery(ModStorage mods)
{
foreach (var (mod, setting) in mods.Zip(Settings).Where(s => s.Second != null))
((Dictionary<string, ModSettings.SavedSettings>)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod);
((List<ModSettings?>)Settings).Clear();
}
/// <summary>
/// Apply all mod settings from unused settings to the current set of mods.
/// Also fixes invalid settings.
/// </summary>
internal void ApplyModSettings(SaveService saver, ModStorage mods)
{
((List<ModSettings?>)Settings).Capacity = Math.Max(((List<ModSettings?>)Settings).Capacity, mods.Count);
if (mods.Aggregate(false, (current, mod) => current | AddMod(mod)))
saver.ImmediateSave(new ModCollectionSave(mods, this));
}
private ModCollection(Guid id, string name, int index, int changeCounter, int version, List<ModSettings?> appliedSettings,
List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
{
Name = name;
Id = id;
Index = index;
ChangeCounter = changeCounter;
Settings = appliedSettings;
UnusedSettings = settings;
DirectlyInheritsFrom = inheritsFrom;
foreach (var c in DirectlyInheritsFrom)
((List<ModCollection>)c.DirectParentOf).Add(this);
}
}

View file

@ -1,43 +0,0 @@
using OtterGui;
using OtterGui.Extensions;
using Penumbra.Collections.Manager;
namespace Penumbra.Collections;
public struct ModCollectionIdentity(Guid id, LocalCollectionId localId)
{
public const string DefaultCollectionName = "Default";
public const string EmptyCollectionName = "None";
public static readonly ModCollectionIdentity Empty = new(Guid.Empty, LocalCollectionId.Zero, EmptyCollectionName, 0);
public string Name { get; set; } = string.Empty;
public Guid Id { get; } = id;
public LocalCollectionId LocalId { get; } = localId;
/// <summary> The index of the collection is set and kept up-to-date by the CollectionManager. </summary>
public int Index { get; internal set; }
public string Identifier
=> Id.ToString();
public string ShortIdentifier
=> Id.ShortGuid();
/// <summary> Get the short identifier of a collection unless it is a well-known collection name. </summary>
public string AnonymizedName
=> Id == Guid.Empty ? EmptyCollectionName : Name == DefaultCollectionName ? Name : ShortIdentifier;
public override string ToString()
=> Name.Length > 0 ? Name : ShortIdentifier;
public ModCollectionIdentity(Guid id, LocalCollectionId localId, string name, int index)
: this(id, localId)
{
Name = name;
Index = index;
}
public static ModCollectionIdentity New(string name, LocalCollectionId id, int index)
=> new(Guid.NewGuid(), id, name, index);
}

View file

@ -1,92 +0,0 @@
using OtterGui.Filesystem;
namespace Penumbra.Collections;
public struct ModCollectionInheritance
{
public IReadOnlyList<string>? InheritanceByName { get; private set; }
private readonly List<ModCollection> _directlyInheritsFrom = [];
private readonly List<ModCollection> _directlyInheritedBy = [];
private readonly List<ModCollection> _flatHierarchy = [];
public ModCollectionInheritance()
{ }
private ModCollectionInheritance(List<ModCollection> inheritsFrom)
=> _directlyInheritsFrom = [.. inheritsFrom];
public ModCollectionInheritance(IReadOnlyList<string> byName)
=> InheritanceByName = byName;
public ModCollectionInheritance Clone()
=> new(_directlyInheritsFrom);
public IEnumerable<string> Identifiers
=> InheritanceByName ?? _directlyInheritsFrom.Select(c => c.Identity.Identifier);
public IReadOnlyList<string>? ConsumeNames()
{
var ret = InheritanceByName;
InheritanceByName = null;
return ret;
}
public static void UpdateChildren(ModCollection parent)
{
foreach (var inheritance in parent.Inheritance.DirectlyInheritsFrom)
inheritance.Inheritance._directlyInheritedBy.Add(parent);
}
public void AddInheritance(ModCollection inheritor, ModCollection newParent)
{
_directlyInheritsFrom.Add(newParent);
newParent.Inheritance._directlyInheritedBy.Add(inheritor);
UpdateFlattenedInheritance(inheritor);
}
public ModCollection RemoveInheritanceAt(ModCollection inheritor, int idx)
{
var parent = DirectlyInheritsFrom[idx];
_directlyInheritsFrom.RemoveAt(idx);
parent.Inheritance._directlyInheritedBy.Remove(parent);
UpdateFlattenedInheritance(inheritor);
return parent;
}
public bool MoveInheritance(ModCollection inheritor, int from, int to)
{
if (!_directlyInheritsFrom.Move(from, to))
return false;
UpdateFlattenedInheritance(inheritor);
return true;
}
public void RemoveChild(ModCollection child)
=> _directlyInheritedBy.Remove(child);
/// <summary> Contains all direct parent collections this collection inherits settings from. </summary>
public readonly IReadOnlyList<ModCollection> DirectlyInheritsFrom
=> _directlyInheritsFrom;
/// <summary> Contains all direct child collections that inherit from this collection. </summary>
public readonly IReadOnlyList<ModCollection> DirectlyInheritedBy
=> _directlyInheritedBy;
/// <summary>
/// Iterate over all collections inherited from in depth-first order.
/// Skip already visited collections to avoid circular dependencies.
/// </summary>
public readonly IReadOnlyList<ModCollection> FlatHierarchy
=> _flatHierarchy;
public static void UpdateFlattenedInheritance(ModCollection parent)
{
parent.Inheritance._flatHierarchy.Clear();
parent.Inheritance._flatHierarchy.AddRange(InheritedCollections(parent).Distinct());
}
/// <summary> All inherited collections in application order without filtering for duplicates. </summary>
private static IEnumerable<ModCollection> InheritedCollections(ModCollection parent)
=> parent.Inheritance.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(parent);
}

View file

@ -15,7 +15,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
=> fileNames.CollectionFile(modCollection);
public string LogName(string _)
=> modCollection.Identity.AnonymizedName;
=> modCollection.AnonymizedName;
public string TypeName
=> "Collection";
@ -28,23 +28,23 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
j.WriteStartObject();
j.WritePropertyName("Version");
j.WriteValue(ModCollection.CurrentVersion);
j.WritePropertyName(nameof(ModCollectionIdentity.Id));
j.WriteValue(modCollection.Identity.Identifier);
j.WritePropertyName(nameof(ModCollectionIdentity.Name));
j.WriteValue(modCollection.Identity.Name);
j.WritePropertyName("Settings");
j.WritePropertyName(nameof(ModCollection.Id));
j.WriteValue(modCollection.Identifier);
j.WritePropertyName(nameof(ModCollection.Name));
j.WriteValue(modCollection.Name);
j.WritePropertyName(nameof(ModCollection.Settings));
// Write all used and unused settings by mod directory name.
j.WriteStartObject();
var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.Settings.Unused.Count);
var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.UnusedSettings.Count);
for (var i = 0; i < modCollection.Settings.Count; ++i)
{
var settings = modCollection.GetOwnSettings(i);
var settings = modCollection.Settings[i];
if (settings != null)
list.Add((modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, modStorage[i])));
}
list.AddRange(modCollection.Settings.Unused.Select(kvp => (kvp.Key, kvp.Value)));
list.AddRange(modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value)));
list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase));
foreach (var (modDir, settings) in list)
@ -57,7 +57,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
// Inherit by collection name.
j.WritePropertyName("Inheritance");
x.Serialize(j, modCollection.Inheritance.Identifiers);
x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier));
j.WriteEndObject();
}
@ -79,10 +79,10 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
{
var obj = JObject.Parse(File.ReadAllText(file.FullName));
version = obj["Version"]?.ToObject<int>() ?? 0;
name = obj[nameof(ModCollectionIdentity.Name)]?.ToObject<string>() ?? string.Empty;
id = obj[nameof(ModCollectionIdentity.Id)]?.ToObject<Guid>() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty);
name = obj[nameof(ModCollection.Name)]?.ToObject<string>() ?? string.Empty;
id = obj[nameof(ModCollection.Id)]?.ToObject<Guid>() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty);
// Custom deserialization that is converted with the constructor.
settings = obj["Settings"]?.ToObject<Dictionary<string, ModSettings.SavedSettings>>() ?? settings;
settings = obj[nameof(ModCollection.Settings)]?.ToObject<Dictionary<string, ModSettings.SavedSettings>>() ?? settings;
inheritance = obj["Inheritance"]?.ToObject<List<string>>() ?? inheritance;
return true;
}

View file

@ -1,98 +0,0 @@
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections;
public readonly struct ModSettingProvider
{
private ModSettingProvider(IEnumerable<FullModSettings> settings, Dictionary<string, ModSettings.SavedSettings> unusedSettings)
{
_settings = settings.Select(s => s.DeepCopy()).ToList();
_unused = unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy());
}
public ModSettingProvider()
{ }
public static ModSettingProvider Empty(int count)
=> new(Enumerable.Repeat(FullModSettings.Empty, count), []);
public ModSettingProvider(Dictionary<string, ModSettings.SavedSettings> allSettings)
=> _unused = allSettings;
private readonly List<FullModSettings> _settings = [];
/// <summary> Settings for deleted mods will be kept via the mods identifier (directory name). </summary>
private readonly Dictionary<string, ModSettings.SavedSettings> _unused = [];
public int Count
=> _settings.Count;
public bool RemoveUnused(string key)
=> _unused.Remove(key);
internal void Set(Index index, ModSettings? settings)
=> _settings[index] = _settings[index] with { Settings = settings };
internal void SetTemporary(Index index, TemporaryModSettings? settings)
=> _settings[index] = _settings[index] with { TempSettings = settings };
internal void SetAll(Index index, FullModSettings settings)
=> _settings[index] = settings;
public IReadOnlyList<FullModSettings> Settings
=> _settings;
public IReadOnlyDictionary<string, ModSettings.SavedSettings> Unused
=> _unused;
public ModSettingProvider Clone()
=> new(_settings, _unused);
/// <summary> Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. </summary>
internal bool AddMod(Mod mod)
{
if (_unused.Remove(mod.ModPath.Name, out var save))
{
var ret = save.ToSettings(mod, out var settings);
_settings.Add(new FullModSettings(settings));
return ret;
}
_settings.Add(FullModSettings.Empty);
return false;
}
/// <summary> Move settings from the current mod list to the unused mod settings. </summary>
internal void RemoveMod(Mod mod)
{
var settings = _settings[mod.Index];
if (settings.Settings != null)
_unused[mod.ModPath.Name] = new ModSettings.SavedSettings(settings.Settings, mod);
_settings.RemoveAt(mod.Index);
}
/// <summary> Move all settings to unused settings for rediscovery. </summary>
internal void PrepareModDiscovery(ModStorage mods)
{
foreach (var (mod, setting) in mods.Zip(_settings).Where(s => s.Second.Settings != null))
_unused[mod.ModPath.Name] = new ModSettings.SavedSettings(setting.Settings!, mod);
_settings.Clear();
}
/// <summary>
/// Apply all mod settings from unused settings to the current set of mods.
/// Also fixes invalid settings.
/// </summary>
internal void ApplyModSettings(ModCollection parent, SaveService saver, ModStorage mods)
{
_settings.Capacity = Math.Max(_settings.Capacity, mods.Count);
var settings = this;
if (mods.Aggregate(false, (current, mod) => current | settings.AddMod(mod)))
saver.ImmediateSave(new ModCollectionSave(mods, parent));
}
}

View file

@ -23,7 +23,7 @@ public readonly struct ResolveData(ModCollection collection, nint gameObject)
{ }
public override string ToString()
=> ModCollection.Identity.Name;
=> ModCollection.Name;
}
public static class ResolveDataExtensions

View file

@ -1,10 +1,8 @@
using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Dalamud.Bindings.ImGui;
using ImGuiNET;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -12,12 +10,12 @@ using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Knowledge;
namespace Penumbra;
public class CommandHandler : IDisposable, IApiService
public class CommandHandler : IDisposable
{
private const string CommandName = "/penumbra";
@ -31,12 +29,11 @@ public class CommandHandler : IDisposable, IApiService
private readonly CollectionManager _collectionManager;
private readonly Penumbra _penumbra;
private readonly CollectionEditor _collectionEditor;
private readonly KnowledgeWindow _knowledgeWindow;
public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService,
Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors,
Penumbra penumbra,
CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow)
Configuration config,
ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra,
CollectionEditor collectionEditor)
{
_commandManager = commandManager;
_redrawService = redrawService;
@ -48,7 +45,6 @@ public class CommandHandler : IDisposable, IApiService
_chat = chat;
_penumbra = penumbra;
_collectionEditor = collectionEditor;
_knowledgeWindow = knowledgeWindow;
framework.RunOnFrameworkThread(() =>
{
if (_commandManager.Commands.ContainsKey(CommandName))
@ -73,7 +69,7 @@ public class CommandHandler : IDisposable, IApiService
var argumentList = arguments.Split(' ', 2);
arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty;
_ = argumentList[0].ToLowerInvariant() switch
var _ = argumentList[0].ToLowerInvariant() switch
{
"window" => ToggleWindow(arguments),
"enable" => SetPenumbraState(arguments, true),
@ -87,8 +83,6 @@ public class CommandHandler : IDisposable, IApiService
"collection" => SetCollection(arguments),
"mod" => SetMod(arguments),
"bulktag" => SetTag(arguments),
"clearsettings" => ClearSettings(arguments),
"knowledge" => HandleKnowledge(arguments),
_ => PrintHelp(argumentList[0]),
};
}
@ -127,21 +121,6 @@ public class CommandHandler : IDisposable, IApiService
_chat.Print(new SeStringBuilder()
.AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.")
.BuiltString);
_chat.Print(new SeStringBuilder()
.AddCommand("clearsettings",
"Clear all temporary settings applied manually through Penumbra in the current or all collections. Use with 'all' parameter for all.")
.BuiltString);
return true;
}
private bool ClearSettings(string arguments)
{
if (arguments.Trim().ToLowerInvariant() is "all")
foreach (var collection in _collectionManager.Storage)
_collectionEditor.ClearTemporarySettings(collection);
else
_collectionEditor.ClearTemporarySettings(_collectionManager.Active.Current);
return true;
}
@ -325,7 +304,7 @@ public class CommandHandler : IDisposable, IApiService
identifiers = _actors.FromUserString(split[2], false);
}
}
catch (ActorIdentifierFactory.IdentifierParseError e)
catch (ActorManager.IdentifierParseError e)
{
_chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true)
.AddText($" could not be converted to an identifier. {e.Message}")
@ -342,7 +321,7 @@ public class CommandHandler : IDisposable, IApiService
{
_chat.Print(collection == null
? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned"
: $"{collection.Identity.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
: $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
continue;
}
@ -379,13 +358,13 @@ public class CommandHandler : IDisposable, IApiService
}
Print(
$"Removed {oldCollection.Identity.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}");
$"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}");
anySuccess = true;
continue;
}
_collectionManager.Active.SetCollection(collection!, type, individualIndex);
Print($"Assigned {collection!.Identity.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
}
return anySuccess;
@ -396,18 +375,16 @@ public class CommandHandler : IDisposable, IApiService
if (arguments.Length == 0)
{
var seString = new SeStringBuilder()
.AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|").AddGreen("setting").AddBlue("]").AddText(" ")
.AddYellow("[Collection Name]")
.AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle]").AddText(" ").AddYellow("[Collection Name]")
.AddText(" | ")
.AddPurple("[Mod Name or Mod Directory Name]")
.AddGreen(" <| [Option Group Name] | [Option1;Option2;...]>");
.AddPurple("[Mod Name or Mod Directory Name]");
_chat.Print(seString.BuiltString);
return true;
}
var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var nameSplit = split.Length != 2
? []
? Array.Empty<string>()
: split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (nameSplit.Length != 2)
{
@ -425,24 +402,6 @@ public class CommandHandler : IDisposable, IApiService
if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty)
return false;
var groupName = string.Empty;
var optionNames = Array.Empty<string>();
if (state is 4)
{
var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (split2.Length < 2)
{
_chat.Print(
"Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options.");
return false;
}
nameSplit[1] = split2[0];
groupName = split2[1];
if (split2.Length == 3)
optionNames = split2[2].Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}
if (!_modManager.TryGetMod(nameSplit[1], nameSplit[1], out var mod))
{
_chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" does not exist.")
@ -450,35 +409,12 @@ public class CommandHandler : IDisposable, IApiService
return false;
}
if (state < 4)
{
if (HandleModState(state, collection!, mod))
return true;
_chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true)
.AddText("already had the desired state in collection ")
.AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString);
return false;
}
switch (ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIndex, out var setting))
{
case PenumbraApiEc.OptionGroupMissing:
_chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" has no group ")
.AddGreen(groupName, true).AddText(".").BuiltString);
return false;
case PenumbraApiEc.OptionMissing:
_chat.Print(new SeStringBuilder().AddText("Not all set options in the mod ").AddRed(nameSplit[1], true)
.AddText(" could be found in group ").AddGreen(groupName, true).AddText(".").BuiltString);
return false;
case PenumbraApiEc.Success:
_collectionEditor.SetModSetting(collection!, mod, groupIndex, setting);
Print(() => new SeStringBuilder().AddText("Changed settings of group ").AddGreen(groupName, true).AddText(" in mod ")
.AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString);
return true;
}
.AddYellow(collection!.Name, true).AddText(".").BuiltString);
return false;
}
@ -560,7 +496,7 @@ public class CommandHandler : IDisposable, IApiService
changes |= HandleModState(state, collection!, mod);
if (!changes)
Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Identity.Name, true)
Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Name, true)
.AddText(".").BuiltString);
return true;
@ -575,7 +511,7 @@ public class CommandHandler : IDisposable, IApiService
return true;
}
collection = string.Equals(lowerName, ModCollection.Empty.Identity.Name, StringComparison.OrdinalIgnoreCase)
collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase)
? ModCollection.Empty
: _collectionManager.Storage.ByIdentifier(lowerName, out var c)
? c
@ -616,14 +552,12 @@ public class CommandHandler : IDisposable, IApiService
"toggle" => 2,
"inherit" => 3,
"inherited" => 3,
"setting" => 4,
"settings" => 4,
_ => -1,
};
private bool HandleModState(int settingState, ModCollection collection, Mod mod)
{
var settings = collection.GetOwnSettings(mod.Index);
var settings = collection.Settings[mod.Index];
switch (settingState)
{
case 0:
@ -631,7 +565,7 @@ public class CommandHandler : IDisposable, IApiService
return false;
Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection.Identity.Name, true)
.AddYellow(collection.Name, true)
.AddText(".").BuiltString);
return true;
@ -640,7 +574,7 @@ public class CommandHandler : IDisposable, IApiService
return false;
Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection.Identity.Name, true)
.AddYellow(collection.Name, true)
.AddText(".").BuiltString);
return true;
@ -651,7 +585,7 @@ public class CommandHandler : IDisposable, IApiService
Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true)
.AddText(" in collection ")
.AddYellow(collection.Identity.Name, true)
.AddYellow(collection.Name, true)
.AddText(".").BuiltString);
return true;
@ -660,7 +594,7 @@ public class CommandHandler : IDisposable, IApiService
return false;
Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ")
.AddYellow(collection.Identity.Name, true)
.AddYellow(collection.Name, true)
.AddText(" to inherit.").BuiltString);
return true;
}
@ -685,10 +619,4 @@ public class CommandHandler : IDisposable, IApiService
if (_config.PrintSuccessfulCommandsToChat)
_chat.Print(text());
}
private bool HandleKnowledge(string arguments)
{
_knowledgeWindow.Toggle();
return true;
}
}

View file

@ -1,7 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.GameData.Data;
namespace Penumbra.Communication;
@ -12,7 +11,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the clicked object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData, ChangedItemClick.Priority>(nameof(ChangedItemClick))
public sealed class ChangedItemClick() : EventWrapper<MouseButton, object?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
{
public enum Priority
{

View file

@ -1,6 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.GameData.Data;
namespace Penumbra.Communication;
@ -10,7 +9,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData, ChangedItemHover.Priority>(nameof(ChangedItemHover))
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{
public enum Priority
{

View file

@ -1,23 +0,0 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Interop.Services;
namespace Penumbra.Communication;
/// <summary>
/// Triggered when the Character Utility becomes ready.
/// </summary>
public sealed class CharacterUtilityFinished() : EventWrapper<CharacterUtilityFinished.Priority>(nameof(CharacterUtilityFinished))
{
public enum Priority
{
/// <seealso cref="CharacterUtility"/>
OnFinishedLoading = int.MaxValue,
/// <seealso cref="IpcProviders.OnCharacterUtilityReady"/>
IpcProvider = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager"/>
CollectionCacheManager = 0,
}
}

View file

@ -46,8 +46,5 @@ public sealed class CollectionChange()
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnCollectionChange"/>
ModSelection = 10,
}
}

View file

@ -23,8 +23,5 @@ public sealed class CollectionInheritanceChanged()
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnInheritanceChange"/>
ModSelection = 10,
}
}

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Services;
@ -18,7 +19,7 @@ public sealed class CreatingCharacterBase()
{
public enum Priority
{
/// <seealso cref="GameStateApi.CreatingCharacterBase"/>
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
Api = 0,
/// <seealso cref="CrashHandlerService.OnCreatingCharacterBase"/>

View file

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

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
@ -23,7 +24,7 @@ public sealed class ModSettingChanged()
{
public enum Priority
{
/// <seealso cref="ModSettingsApi.OnModSettingChange"/>
/// <seealso cref="PenumbraApi.OnModSettingChange"/>
Api = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModSettingChange"/>
@ -34,8 +35,5 @@ public sealed class ModSettingChanged()
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnSettingChange"/>
ModSelection = 10,
}
}

View file

@ -6,11 +6,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
/// <item>Parameter is the associated game object. </item>
/// </list> </summary>
public sealed class MtrlLoaded() : EventWrapper<nint, nint, MtrlLoaded.Priority>(nameof(MtrlLoaded))
public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.Priority>(nameof(MtrlShpkLoaded))
{
public enum Priority
{
/// <seealso cref="Interop.Hooks.PostProcessing.ShaderReplacementFixer.OnMtrlLoaded"/>
/// <seealso cref="Interop.Services.ShaderReplacementFixer.OnMtrlShpkLoaded"/>
ShaderReplacementFixer = 0,
}
}

View file

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

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