diff --git a/OtterGui b/OtterGui
index 4673e93f..9599c806 160000
--- a/OtterGui
+++ b/OtterGui
@@ -1 +1 @@
-Subproject commit 4673e93f5165108a7f5b91236406d527f16384a5
+Subproject commit 9599c806877e2972f964dfa68e5207cf3a8f2b84
diff --git a/Penumbra.Api b/Penumbra.Api
index 8787efc8..e5c8f544 160000
--- a/Penumbra.Api
+++ b/Penumbra.Api
@@ -1 +1 @@
-Subproject commit 8787efc8fc897dfbb4515ebbabbcd5e6f54d1b42
+Subproject commit e5c8f5446879e2e0e541eb4d8fee15e98b1885bc
diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
index c92a14fd..3446530a 100644
--- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
+++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
@@ -24,7 +24,7 @@ public record struct VfxFuncInvokedEntry(
string InvocationType,
string CharacterName,
string CharacterAddress,
- string CollectionName) : ICrashDataEntry;
+ Guid CollectionId) : ICrashDataEntry;
/// Only expose the write interface for the buffer.
public interface IAnimationInvocationBufferWriter
@@ -32,19 +32,19 @@ public interface IAnimationInvocationBufferWriter
/// Write a line into the buffer with the given data.
/// The address of the related character, if known.
/// The name of the related character, anonymized or relying on index if unavailable, if known.
- /// The name of the associated collection. Not anonymized.
+ /// The GUID of the associated collection.
/// The type of VFX func called.
- public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type);
+ public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, AnimationInvocationType type);
}
internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 64;
- private const int _lineCapacity = 256;
+ private const int _lineCapacity = 128;
private const string _name = "Penumbra.AnimationInvocation";
- public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type)
+ public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, AnimationInvocationType type)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
@@ -53,10 +53,10 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, (int)type);
accessor.Write(16, characterAddress);
- var span = GetSpan(accessor, 24, 104);
+ var span = GetSpan(accessor, 24, 16);
+ collectionId.TryWriteBytes(span);
+ span = GetSpan(accessor, 40);
WriteSpan(characterName, span);
- span = GetSpan(accessor, 128);
- WriteString(collectionName, span);
}
}
@@ -68,13 +68,13 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
- var line = GetLine(i);
- var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
- var thread = BitConverter.ToInt32(line[8..]);
- var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]);
- var address = BitConverter.ToUInt64(line[16..]);
- var characterName = ReadString(line[24..]);
- var collectionName = ReadString(line[128..]);
+ var line = GetLine(i);
+ var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
+ var thread = BitConverter.ToInt32(line[8..]);
+ var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]);
+ var address = BitConverter.ToUInt64(line[16..]);
+ var collectionId = new Guid(line[24..40]);
+ var characterName = ReadString(line[40..]);
yield return new JsonObject()
{
[nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
@@ -83,7 +83,7 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation
[nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type),
[nameof(VfxFuncInvokedEntry.CharacterName)] = characterName,
[nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"),
- [nameof(VfxFuncInvokedEntry.CollectionName)] = collectionName,
+ [nameof(VfxFuncInvokedEntry.CollectionId)] = collectionId,
};
}
}
diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
index d83c6e6c..4036455d 100644
--- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
+++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
@@ -8,8 +8,8 @@ public interface ICharacterBaseBufferWriter
/// Write a line into the buffer with the given data.
/// The address of the related character, if known.
/// The name of the related character, anonymized or relying on index if unavailable, if known.
- /// The name of the associated collection. Not anonymized.
- public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName);
+ /// The GUID of the associated collection.
+ public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId);
}
/// The full crash entry for a loaded character base.
@@ -19,27 +19,27 @@ public record struct CharacterLoadedEntry(
int ThreadId,
string CharacterName,
string CharacterAddress,
- string CollectionName) : ICrashDataEntry;
+ Guid CollectionId) : ICrashDataEntry;
internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader
{
- private const int _version = 1;
- private const int _lineCount = 10;
- private const int _lineCapacity = 256;
- private const string _name = "Penumbra.CharacterBase";
+ private const int _version = 1;
+ private const int _lineCount = 10;
+ private const int _lineCapacity = 128;
+ private const string _name = "Penumbra.CharacterBase";
- public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName)
+ public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
- accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
- accessor.Write(8, Environment.CurrentManagedThreadId);
+ accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+ accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
- var span = GetSpan(accessor, 20, 108);
+ var span = GetSpan(accessor, 20, 16);
+ collectionId.TryWriteBytes(span);
+ span = GetSpan(accessor, 36);
WriteSpan(characterName, span);
- span = GetSpan(accessor, 128);
- WriteString(collectionName, span);
}
}
@@ -48,20 +48,20 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
- var line = GetLine(i);
- var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
- var thread = BitConverter.ToInt32(line[8..]);
- var address = BitConverter.ToUInt64(line[12..]);
- var characterName = ReadString(line[20..]);
- var collectionName = ReadString(line[128..]);
+ var line = GetLine(i);
+ var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
+ var thread = BitConverter.ToInt32(line[8..]);
+ var address = BitConverter.ToUInt64(line[12..]);
+ var collectionId = new Guid(line[20..36]);
+ var characterName = ReadString(line[36..]);
yield return new JsonObject
{
- [nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
- [nameof(CharacterLoadedEntry.Timestamp)] = timestamp,
- [nameof(CharacterLoadedEntry.ThreadId)] = thread,
- [nameof(CharacterLoadedEntry.CharacterName)] = characterName,
+ [nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
+ [nameof(CharacterLoadedEntry.Timestamp)] = timestamp,
+ [nameof(CharacterLoadedEntry.ThreadId)] = thread,
+ [nameof(CharacterLoadedEntry.CharacterName)] = characterName,
[nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"),
- [nameof(CharacterLoadedEntry.CollectionName)] = collectionName,
+ [nameof(CharacterLoadedEntry.CollectionId)] = collectionId,
};
}
}
diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
index 6c774e4b..03f63ba4 100644
--- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
+++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
@@ -8,10 +8,10 @@ public interface IModdedFileBufferWriter
/// Write a line into the buffer with the given data.
/// The address of the related character, if known.
/// The name of the related character, anonymized or relying on index if unavailable, if known.
- /// The name of the associated collection. Not anonymized.
+ /// The GUID of the associated collection.
/// The file name as requested by the game.
/// The actual modded file name loaded.
- public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName,
+ public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, ReadOnlySpan requestedFileName,
ReadOnlySpan actualFileName);
}
@@ -22,33 +22,33 @@ public record struct ModdedFileLoadedEntry(
int ThreadId,
string CharacterName,
string CharacterAddress,
- string CollectionName,
+ Guid CollectionId,
string RequestedFileName,
string ActualFileName) : ICrashDataEntry;
internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader
{
- private const int _version = 1;
- private const int _lineCount = 128;
- private const int _lineCapacity = 1024;
- private const string _name = "Penumbra.ModdedFile";
+ private const int _version = 1;
+ private const int _lineCount = 128;
+ private const int _lineCapacity = 1024;
+ private const string _name = "Penumbra.ModdedFile";
- public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName,
+ public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, ReadOnlySpan requestedFileName,
ReadOnlySpan actualFileName)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
- accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
- accessor.Write(8, Environment.CurrentManagedThreadId);
+ accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+ accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
- var span = GetSpan(accessor, 20, 80);
+ var span = GetSpan(accessor, 20, 16);
+ collectionId.TryWriteBytes(span);
+ span = GetSpan(accessor, 36, 80);
WriteSpan(characterName, span);
- span = GetSpan(accessor, 92, 80);
- WriteString(collectionName, span);
- span = GetSpan(accessor, 172, 260);
+ span = GetSpan(accessor, 116, 260);
WriteSpan(requestedFileName, span);
- span = GetSpan(accessor, 432);
+ span = GetSpan(accessor, 376);
WriteSpan(actualFileName, span);
}
}
@@ -61,24 +61,24 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
- var line = GetLine(i);
- var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
- var thread = BitConverter.ToInt32(line[8..]);
- var address = BitConverter.ToUInt64(line[12..]);
- var characterName = ReadString(line[20..]);
- var collectionName = ReadString(line[92..]);
- var requestedFileName = ReadString(line[172..]);
- var actualFileName = ReadString(line[432..]);
+ var line = GetLine(i);
+ var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
+ var thread = BitConverter.ToInt32(line[8..]);
+ var address = BitConverter.ToUInt64(line[12..]);
+ var collectionId = new Guid(line[20..36]);
+ var characterName = ReadString(line[36..]);
+ var requestedFileName = ReadString(line[116..]);
+ var actualFileName = ReadString(line[376..]);
yield return new JsonObject()
{
- [nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
- [nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp,
- [nameof(ModdedFileLoadedEntry.ThreadId)] = thread,
- [nameof(ModdedFileLoadedEntry.CharacterName)] = characterName,
- [nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"),
- [nameof(ModdedFileLoadedEntry.CollectionName)] = collectionName,
+ [nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
+ [nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp,
+ [nameof(ModdedFileLoadedEntry.ThreadId)] = thread,
+ [nameof(ModdedFileLoadedEntry.CharacterName)] = characterName,
+ [nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"),
+ [nameof(ModdedFileLoadedEntry.CollectionId)] = collectionId,
[nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName,
- [nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
+ [nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
};
}
}
diff --git a/Penumbra.GameData b/Penumbra.GameData
index 45679aa3..60222d79 160000
--- a/Penumbra.GameData
+++ b/Penumbra.GameData
@@ -1 +1 @@
-Subproject commit 45679aa32cc37b59f5eeb7cf6bf5a3ea36c626e0
+Subproject commit 60222d79420662fb8e9960a66e262a380fcaf186
diff --git a/Penumbra/Api/Api/ApiHelpers.cs b/Penumbra/Api/Api/ApiHelpers.cs
new file mode 100644
index 00000000..32a3956f
--- /dev/null
+++ b/Penumbra/Api/Api/ApiHelpers.cs
@@ -0,0 +1,76 @@
+using OtterGui.Log;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+using Penumbra.GameData.Actors;
+using Penumbra.GameData.Interop;
+using Penumbra.Interop.PathResolving;
+
+namespace Penumbra.Api.Api;
+
+public class ApiHelpers(
+ CollectionManager collectionManager,
+ ObjectManager objects,
+ CollectionResolver collectionResolver,
+ ActorManager actors) : IApiService
+{
+ /// Return the associated identifier for an object given by its index.
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal ActorIdentifier AssociatedIdentifier(int gameObjectIdx)
+ {
+ if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
+ return ActorIdentifier.Invalid;
+
+ var ptr = objects[gameObjectIdx];
+ return actors.FromObject(ptr, out _, false, true, true);
+ }
+
+ ///
+ /// Return the collection associated to a current game object. If it does not exist, return the default collection.
+ /// If the index is invalid, returns false and the default collection.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection)
+ {
+ collection = collectionManager.Active.Default;
+ if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount)
+ return false;
+
+ var ptr = objects[gameObjectIdx];
+ var data = collectionResolver.IdentifyCollection(ptr.AsObject, false);
+ if (data.Valid)
+ collection = data.ModCollection;
+
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown")
+ {
+ Penumbra.Log.Debug(
+ $"[{name}] Called with {args}, returned {ec}.");
+ return ec;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal static LazyString Args(params object[] arguments)
+ {
+ if (arguments.Length == 0)
+ return new LazyString(() => "no arguments");
+
+ return new LazyString(() =>
+ {
+ var sb = new StringBuilder();
+ for (var i = 0; i < arguments.Length / 2; ++i)
+ {
+ sb.Append(arguments[2 * i]);
+ sb.Append(" = ");
+ sb.Append(arguments[2 * i + 1]);
+ sb.Append(", ");
+ }
+
+ return sb.ToString(0, sb.Length - 2);
+ });
+ }
+}
diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs
new file mode 100644
index 00000000..de704460
--- /dev/null
+++ b/Penumbra/Api/Api/CollectionApi.cs
@@ -0,0 +1,141 @@
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+
+namespace Penumbra.Api.Api;
+
+public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService
+{
+ public Dictionary GetCollections()
+ => collections.Storage.ToDictionary(c => c.Id, c => c.Name);
+
+ public Dictionary GetChangedItemsForCollection(Guid collectionId)
+ {
+ try
+ {
+ if (!collections.Storage.ById(collectionId, out var collection))
+ collection = ModCollection.Empty;
+
+ if (collection.HasCache)
+ return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2);
+
+ Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
+ return [];
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error($"Could not obtain Changed Items for {collectionId}:\n{e}");
+ throw;
+ }
+ }
+
+ public (Guid Id, string Name)? GetCollection(ApiCollectionType type)
+ {
+ if (!Enum.IsDefined(type))
+ return null;
+
+ var collection = collections.Active.ByType((CollectionType)type);
+ return collection == null ? null : (collection.Id, collection.Name);
+ }
+
+ internal (Guid Id, string Name)? GetCollection(byte type)
+ => GetCollection((ApiCollectionType)type);
+
+ public (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) GetCollectionForObject(int gameObjectIdx)
+ {
+ var id = helpers.AssociatedIdentifier(gameObjectIdx);
+ if (!id.IsValid)
+ return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name));
+
+ if (collections.Active.Individuals.TryGetValue(id, out var collection))
+ return (true, true, (collection.Id, collection.Name));
+
+ helpers.AssociatedCollection(gameObjectIdx, out collection);
+ return (true, false, (collection.Id, collection.Name));
+ }
+
+ public Guid[] GetCollectionByName(string name)
+ => 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)
+ {
+ if (!Enum.IsDefined(type))
+ return (PenumbraApiEc.InvalidArgument, null);
+
+ var oldCollection = collections.Active.ByType((CollectionType)type);
+ var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?();
+ if (collectionId == null)
+ {
+ if (old == null)
+ return (PenumbraApiEc.NothingChanged, old);
+
+ if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface)
+ return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
+
+ collections.Active.RemoveSpecialCollection((CollectionType)type);
+ return (PenumbraApiEc.Success, old);
+ }
+
+ if (!collections.Storage.ById(collectionId.Value, out var collection))
+ return (PenumbraApiEc.CollectionMissing, old);
+
+ if (old == null)
+ {
+ if (!allowCreateNew)
+ return (PenumbraApiEc.AssignmentCreationDisallowed, old);
+
+ collections.Active.CreateSpecialCollection((CollectionType)type);
+ }
+ else if (old.Value.Item1 == collection.Id)
+ {
+ return (PenumbraApiEc.NothingChanged, old);
+ }
+
+ collections.Active.SetCollection(collection, (CollectionType)type);
+ return (PenumbraApiEc.Success, old);
+ }
+
+ public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollectionForObject(int gameObjectIdx, Guid? collectionId,
+ bool allowCreateNew, bool allowDelete)
+ {
+ var id = helpers.AssociatedIdentifier(gameObjectIdx);
+ if (!id.IsValid)
+ 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.Id, oldCollection.Name) : new ValueTuple?();
+ if (collectionId == null)
+ {
+ if (old == null)
+ return (PenumbraApiEc.NothingChanged, old);
+
+ if (!allowDelete)
+ return (PenumbraApiEc.AssignmentDeletionDisallowed, old);
+
+ var idx = collections.Active.Individuals.Index(id);
+ collections.Active.RemoveIndividualCollection(idx);
+ return (PenumbraApiEc.Success, old);
+ }
+
+ if (!collections.Storage.ById(collectionId.Value, out var collection))
+ return (PenumbraApiEc.CollectionMissing, old);
+
+ if (old == null)
+ {
+ if (!allowCreateNew)
+ return (PenumbraApiEc.AssignmentCreationDisallowed, old);
+
+ var ids = collections.Active.Individuals.GetGroup(id);
+ collections.Active.CreateIndividualCollection(ids);
+ }
+ else if (old.Value.Item1 == collection.Id)
+ {
+ return (PenumbraApiEc.NothingChanged, old);
+ }
+
+ collections.Active.SetCollection(collection, CollectionType.Individual, collections.Active.Individuals.Index(id));
+ return (PenumbraApiEc.Success, old);
+ }
+}
diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs
new file mode 100644
index 00000000..93345053
--- /dev/null
+++ b/Penumbra/Api/Api/EditingApi.cs
@@ -0,0 +1,40 @@
+using OtterGui.Services;
+using Penumbra.Import.Textures;
+using TextureType = Penumbra.Api.Enums.TextureType;
+
+namespace Penumbra.Api.Api;
+
+public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IApiService
+{
+ public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps)
+ => textureType switch
+ {
+ TextureType.Png => textureManager.SavePng(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),
+ TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile),
+ TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile),
+ 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),
+ _ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
+ };
+
+ // @formatter:off
+ public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps)
+ => textureType switch
+ {
+ TextureType.Png => textureManager.SavePng(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),
+ TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
+ 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),
+ _ => Task.FromException(new Exception($"Invalid input value {textureType}.")),
+ };
+ // @formatter:on
+}
diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs
new file mode 100644
index 00000000..becb55ee
--- /dev/null
+++ b/Penumbra/Api/Api/GameStateApi.cs
@@ -0,0 +1,82 @@
+using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Collections;
+using Penumbra.Interop.PathResolving;
+using Penumbra.Interop.ResourceLoading;
+using Penumbra.Interop.Structs;
+using Penumbra.Services;
+using Penumbra.String.Classes;
+
+namespace Penumbra.Api.Api;
+
+public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
+{
+ private readonly CommunicatorService _communicator;
+ private readonly CollectionResolver _collectionResolver;
+ private readonly CutsceneService _cutsceneService;
+ private readonly ResourceLoader _resourceLoader;
+
+ public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
+ ResourceLoader resourceLoader)
+ {
+ _communicator = communicator;
+ _collectionResolver = collectionResolver;
+ _cutsceneService = cutsceneService;
+ _resourceLoader = resourceLoader;
+ _resourceLoader.ResourceLoaded += OnResourceLoaded;
+ _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
+ }
+
+ public unsafe void Dispose()
+ {
+ _resourceLoader.ResourceLoaded -= OnResourceLoaded;
+ _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
+ }
+
+ public event CreatedCharacterBaseDelegate? CreatedCharacterBase;
+ public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved;
+
+ public event CreatingCharacterBaseDelegate? CreatingCharacterBase
+ {
+ add
+ {
+ if (value == null)
+ return;
+
+ _communicator.CreatingCharacterBase.Subscribe(new Action(value),
+ Communication.CreatingCharacterBase.Priority.Api);
+ }
+ remove
+ {
+ if (value == null)
+ return;
+
+ _communicator.CreatingCharacterBase.Unsubscribe(new Action(value));
+ }
+ }
+
+ public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject)
+ {
+ var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
+ return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name));
+ }
+
+ public int GetCutsceneParentIndex(int actorIdx)
+ => _cutsceneService.GetParentIndex(actorIdx);
+
+ public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
+ => _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.InvalidArgument;
+
+ private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
+ {
+ if (resolveData.AssociatedGameObject != nint.Zero)
+ GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(),
+ manipulatedPath?.ToString() ?? originalPath.ToString());
+ }
+
+ private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject)
+ => CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject);
+}
diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs
new file mode 100644
index 00000000..c467df58
--- /dev/null
+++ b/Penumbra/Api/Api/MetaApi.cs
@@ -0,0 +1,23 @@
+using OtterGui;
+using OtterGui.Services;
+using Penumbra.Interop.PathResolving;
+using Penumbra.Meta.Manipulations;
+
+namespace Penumbra.Api.Api;
+
+public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
+{
+ public string GetPlayerMetaManipulations()
+ {
+ var collection = collectionResolver.PlayerCollection();
+ var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
+ return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
+ }
+
+ public string GetMetaManipulations(int gameObjectIdx)
+ {
+ helpers.AssociatedCollection(gameObjectIdx, out var collection);
+ var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
+ return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
+ }
+}
diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs
new file mode 100644
index 00000000..2604a49d
--- /dev/null
+++ b/Penumbra/Api/Api/ModSettingsApi.cs
@@ -0,0 +1,282 @@
+using OtterGui;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+using Penumbra.Communication;
+using Penumbra.Interop.PathResolving;
+using Penumbra.Mods;
+using Penumbra.Mods.Editor;
+using Penumbra.Mods.Manager;
+using Penumbra.Mods.Subclasses;
+using Penumbra.Services;
+
+namespace Penumbra.Api.Api;
+
+public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
+{
+ private readonly CollectionResolver _collectionResolver;
+ private readonly ModManager _modManager;
+ private readonly CollectionManager _collectionManager;
+ private readonly CollectionEditor _collectionEditor;
+ private readonly CommunicatorService _communicator;
+
+ public ModSettingsApi(CollectionResolver collectionResolver,
+ ModManager modManager,
+ CollectionManager collectionManager,
+ CollectionEditor collectionEditor,
+ CommunicatorService communicator)
+ {
+ _collectionResolver = collectionResolver;
+ _modManager = modManager;
+ _collectionManager = collectionManager;
+ _collectionEditor = collectionEditor;
+ _communicator = communicator;
+ _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings);
+ _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api);
+ _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api);
+ _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api);
+ }
+
+ public void Dispose()
+ {
+ _communicator.ModPathChanged.Unsubscribe(OnModPathChange);
+ _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
+ _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited);
+ _communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
+ }
+
+ public event ModSettingChangedDelegate? ModSettingChanged;
+
+ public AvailableModSettings? GetAvailableModSettings(string modDirectory, string modName)
+ {
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return null;
+
+ var dict = new Dictionary(mod.Groups.Count);
+ foreach (var g in mod.Groups)
+ dict.Add(g.Name, (g.Select(o => o.Name).ToArray(), (int)g.Type));
+ return new AvailableModSettings(dict);
+ }
+
+ public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName)
+ => _modManager.TryGetMod(modDirectory, modName, out var mod)
+ ? mod.Groups.ToDictionary(g => g.Name, g => (g.Select(o => o.Name).ToArray(), (int)g.Type))
+ : null;
+
+ public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory,
+ string modName, bool ignoreInheritance)
+ {
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return (PenumbraApiEc.ModMissing, null);
+
+ if (!_collectionManager.Storage.ById(collectionId, out var collection))
+ return (PenumbraApiEc.CollectionMissing, null);
+
+ var settings = collection.Id == Guid.Empty
+ ? null
+ : ignoreInheritance
+ ? collection.Settings[mod.Index]
+ : collection[mod.Index].Settings;
+ if (settings == null)
+ return (PenumbraApiEc.Success, null);
+
+ 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)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit",
+ inherit.ToString());
+
+ if (collectionId == Guid.Empty)
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ if (!_collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
+
+ var ret = _collectionEditor.SetModInheritance(collection, mod, inherit)
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.NothingChanged;
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc TrySetMod(Guid collectionId, string modDirectory, string modName, bool enabled)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled);
+ if (!_collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
+
+ var ret = _collectionEditor.SetModState(collection, mod, enabled)
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.NothingChanged;
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc TrySetModPriority(Guid collectionId, string modDirectory, string modName, int priority)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Priority", priority);
+
+ if (!_collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
+
+ var ret = _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority))
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.NothingChanged;
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc TrySetModSetting(Guid collectionId, string modDirectory, string modName, string optionGroupName, string optionName)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
+ optionGroupName, "OptionName", optionName);
+
+ if (!_collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
+
+ var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
+ if (groupIdx < 0)
+ return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
+
+ var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName);
+ if (optionIdx < 0)
+ return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
+
+ var setting = mod.Groups[groupIdx] switch
+ {
+ MultiModGroup => Setting.Multi(optionIdx),
+ SingleModGroup => Setting.Single(optionIdx),
+ _ => Setting.Zero,
+ };
+ var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting)
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.NothingChanged;
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc TrySetModSettings(Guid collectionId, string modDirectory, string modName, string optionGroupName,
+ IReadOnlyList optionNames)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName",
+ optionGroupName, "#optionNames", optionNames.Count);
+
+ if (!_collectionManager.Storage.ById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, args);
+
+ var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
+ if (groupIdx < 0)
+ return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
+
+ var setting = Setting.Zero;
+ switch (mod.Groups[groupIdx])
+ {
+ case SingleModGroup single:
+ {
+ var optionIdx = optionNames.Count == 0 ? -1 : single.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.IndexOf(o => o.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
+ : PenumbraApiEc.NothingChanged;
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc CopyModSettings(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId.HasValue ? collectionId.Value.ToString() : "NULL",
+ "From", modDirectoryFrom, "To", modDirectoryTo);
+ var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase));
+ var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase));
+ if (collectionId == null)
+ foreach (var collection in _collectionManager.Storage)
+ _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
+ else if (_collectionManager.Storage.ById(collectionId.Value, out var collection))
+ _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo);
+ else
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ return ApiHelpers.Return(PenumbraApiEc.Success, args);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private void TriggerSettingEdited(Mod mod)
+ {
+ var collection = _collectionResolver.PlayerCollection();
+ var (settings, parent) = collection[mod.Index];
+ if (settings is { Enabled: true })
+ ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection);
+ }
+
+ private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
+ {
+ if (type == ModPathChangeType.Reloaded)
+ TriggerSettingEdited(mod);
+ }
+
+ private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited)
+ => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited);
+
+ private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex)
+ {
+ switch (type)
+ {
+ case ModOptionChangeType.GroupDeleted:
+ case ModOptionChangeType.GroupMoved:
+ case ModOptionChangeType.GroupTypeChanged:
+ case ModOptionChangeType.PriorityChanged:
+ case ModOptionChangeType.OptionDeleted:
+ case ModOptionChangeType.OptionMoved:
+ case ModOptionChangeType.OptionFilesChanged:
+ case ModOptionChangeType.OptionFilesAdded:
+ case ModOptionChangeType.OptionSwapsChanged:
+ case ModOptionChangeType.OptionMetaChanged:
+ TriggerSettingEdited(mod);
+ break;
+ }
+ }
+
+ private void OnModFileChanged(Mod mod, FileRegistry file)
+ {
+ if (file.CurrentUsage == 0)
+ return;
+
+ TriggerSettingEdited(mod);
+ }
+}
diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs
new file mode 100644
index 00000000..c1e0c684
--- /dev/null
+++ b/Penumbra/Api/Api/ModsApi.cs
@@ -0,0 +1,132 @@
+using OtterGui.Compression;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Communication;
+using Penumbra.Mods;
+using Penumbra.Mods.Manager;
+using Penumbra.Services;
+
+namespace Penumbra.Api.Api;
+
+public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
+{
+ private readonly CommunicatorService _communicator;
+ private readonly ModManager _modManager;
+ private readonly ModImportManager _modImportManager;
+ private readonly Configuration _config;
+ private readonly ModFileSystem _modFileSystem;
+
+ public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem,
+ CommunicatorService communicator)
+ {
+ _modManager = modManager;
+ _modImportManager = modImportManager;
+ _config = config;
+ _modFileSystem = modFileSystem;
+ _communicator = communicator;
+ _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods);
+ }
+
+ private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
+ {
+ 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.Moved when newDirectory != null && oldDirectory != null:
+ ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
+ break;
+ }
+ }
+
+ public void Dispose()
+ => _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
+
+ public Dictionary GetModList()
+ => _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
+
+ public PenumbraApiEc InstallMod(string modFilePackagePath)
+ {
+ if (!File.Exists(modFilePackagePath))
+ return ApiHelpers.Return(PenumbraApiEc.FileMissing, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
+
+ _modImportManager.AddUnpack(modFilePackagePath);
+ return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath));
+ }
+
+ public PenumbraApiEc ReloadMod(string modDirectory, string modName)
+ {
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.ModMissing, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
+
+ _modManager.ReloadMod(mod);
+ return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
+ }
+
+ public PenumbraApiEc AddMod(string modDirectory)
+ {
+ var args = ApiHelpers.Args("ModDirectory", modDirectory);
+
+ var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory)));
+ if (!dir.Exists)
+ return ApiHelpers.Return(PenumbraApiEc.FileMissing, args);
+
+ if (_modManager.BasePath.FullName != dir.Parent?.FullName)
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ _modManager.AddMod(dir);
+ if (_config.UseFileSystemCompression)
+ new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
+ CompressionAlgorithm.Xpress8K);
+ return ApiHelpers.Return(PenumbraApiEc.Success, args);
+ }
+
+ public PenumbraApiEc DeleteMod(string modDirectory, string modName)
+ {
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
+ return ApiHelpers.Return(PenumbraApiEc.NothingChanged, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
+
+ _modManager.DeleteMod(mod);
+ return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName));
+ }
+
+ public event Action? ModDeleted;
+ public event Action? ModAdded;
+ public event Action? ModMoved;
+
+ public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
+ {
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
+ || !_modFileSystem.FindLeaf(mod, out var leaf))
+ return (PenumbraApiEc.ModMissing, string.Empty, false, false);
+
+ var fullPath = leaf.FullName();
+ var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath);
+ var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name);
+ return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault );
+ }
+
+ public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath)
+ {
+ if (newPath.Length == 0)
+ return PenumbraApiEc.InvalidArgument;
+
+ if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
+ || !_modFileSystem.FindLeaf(mod, out var leaf))
+ return PenumbraApiEc.ModMissing;
+
+ try
+ {
+ _modFileSystem.RenameAndMove(leaf, newPath);
+ return PenumbraApiEc.Success;
+ }
+ catch
+ {
+ return PenumbraApiEc.PathRenameFailed;
+ }
+ }
+}
diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs
new file mode 100644
index 00000000..1d5b1537
--- /dev/null
+++ b/Penumbra/Api/Api/PenumbraApi.cs
@@ -0,0 +1,40 @@
+using OtterGui.Services;
+
+namespace Penumbra.Api.Api;
+
+public class PenumbraApi(
+ CollectionApi collection,
+ EditingApi editing,
+ GameStateApi gameState,
+ MetaApi meta,
+ ModsApi mods,
+ ModSettingsApi modSettings,
+ PluginStateApi pluginState,
+ RedrawApi redraw,
+ ResolveApi resolve,
+ ResourceTreeApi resourceTree,
+ TemporaryApi temporary,
+ UiApi ui) : IDisposable, IApiService, IPenumbraApi
+{
+ public void Dispose()
+ {
+ Valid = false;
+ }
+
+ public (int Breaking, int Feature) ApiVersion
+ => (5, 0);
+
+ public bool Valid { get; private set; } = true;
+ public IPenumbraApiCollection Collection { get; } = collection;
+ public IPenumbraApiEditing Editing { get; } = editing;
+ public IPenumbraApiGameState GameState { get; } = gameState;
+ public IPenumbraApiMeta Meta { get; } = meta;
+ public IPenumbraApiMods Mods { get; } = mods;
+ public IPenumbraApiModSettings ModSettings { get; } = modSettings;
+ public IPenumbraApiPluginState PluginState { get; } = pluginState;
+ public IPenumbraApiRedraw Redraw { get; } = redraw;
+ public IPenumbraApiResolve Resolve { get; } = resolve;
+ public IPenumbraApiResourceTree ResourceTree { get; } = resourceTree;
+ public IPenumbraApiTemporary Temporary { get; } = temporary;
+ public IPenumbraApiUi Ui { get; } = ui;
+}
diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs
new file mode 100644
index 00000000..2e87486f
--- /dev/null
+++ b/Penumbra/Api/Api/PluginStateApi.cs
@@ -0,0 +1,30 @@
+using Newtonsoft.Json;
+using OtterGui.Services;
+using Penumbra.Communication;
+using Penumbra.Services;
+
+namespace Penumbra.Api.Api;
+
+public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService
+{
+ public string GetModDirectory()
+ => config.ModDirectory;
+
+ public string GetConfiguration()
+ => JsonConvert.SerializeObject(config, Formatting.Indented);
+
+ public event Action? ModDirectoryChanged
+ {
+ add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
+ remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
+ }
+
+ public bool GetEnabledState()
+ => config.EnableMods;
+
+ public event Action? EnabledChange
+ {
+ add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
+ remove => communicator.EnabledChanged.Unsubscribe(value!);
+ }
+}
diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs
new file mode 100644
index 00000000..03b42493
--- /dev/null
+++ b/Penumbra/Api/Api/RedrawApi.cs
@@ -0,0 +1,27 @@
+using Dalamud.Game.ClientState.Objects.Types;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Interop.Services;
+
+namespace Penumbra.Api.Api;
+
+public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService
+{
+ public void RedrawObject(int gameObjectIndex, RedrawType setting)
+ => redrawService.RedrawObject(gameObjectIndex, setting);
+
+ public void RedrawObject(string name, RedrawType setting)
+ => redrawService.RedrawObject(name, setting);
+
+ public void RedrawObject(GameObject? gameObject, RedrawType setting)
+ => redrawService.RedrawObject(gameObject, setting);
+
+ public void RedrawAll(RedrawType setting)
+ => redrawService.RedrawAll(setting);
+
+ public event GameObjectRedrawnDelegate? GameObjectRedrawn
+ {
+ add => redrawService.GameObjectRedrawn += value;
+ remove => redrawService.GameObjectRedrawn -= value;
+ }
+}
diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs
new file mode 100644
index 00000000..ec57eba7
--- /dev/null
+++ b/Penumbra/Api/Api/ResolveApi.cs
@@ -0,0 +1,101 @@
+using Dalamud.Plugin.Services;
+using OtterGui.Services;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+using Penumbra.Interop.PathResolving;
+using Penumbra.Mods.Manager;
+using Penumbra.String.Classes;
+
+namespace Penumbra.Api.Api;
+
+public class ResolveApi(
+ ModManager modManager,
+ CollectionManager collectionManager,
+ Configuration config,
+ CollectionResolver collectionResolver,
+ ApiHelpers helpers,
+ IFramework framework) : IPenumbraApiResolve, IApiService
+{
+ public string ResolveDefaultPath(string gamePath)
+ => ResolvePath(gamePath, modManager, collectionManager.Active.Default);
+
+ public string ResolveInterfacePath(string gamePath)
+ => ResolvePath(gamePath, modManager, collectionManager.Active.Interface);
+
+ public string ResolveGameObjectPath(string gamePath, int gameObjectIdx)
+ {
+ helpers.AssociatedCollection(gameObjectIdx, out var collection);
+ return ResolvePath(gamePath, modManager, collection);
+ }
+
+ public string ResolvePlayerPath(string gamePath)
+ => ResolvePath(gamePath, modManager, collectionResolver.PlayerCollection());
+
+ public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIdx)
+ {
+ if (!config.EnableMods)
+ return [moddedPath];
+
+ helpers.AssociatedCollection(gameObjectIdx, out var collection);
+ var ret = collection.ReverseResolvePath(new FullPath(moddedPath));
+ return ret.Select(r => r.ToString()).ToArray();
+ }
+
+ public string[] ReverseResolvePlayerPath(string moddedPath)
+ {
+ if (!config.EnableMods)
+ return [moddedPath];
+
+ var ret = collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(moddedPath));
+ return ret.Select(r => r.ToString()).ToArray();
+ }
+
+ public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse)
+ {
+ if (!config.EnableMods)
+ return (forward, reverse.Select(p => new[]
+ {
+ p,
+ }).ToArray());
+
+ var playerCollection = collectionResolver.PlayerCollection();
+ var resolved = forward.Select(p => ResolvePath(p, modManager, playerCollection)).ToArray();
+ var reverseResolved = playerCollection.ReverseResolvePaths(reverse);
+ return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
+ }
+
+ public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse)
+ {
+ if (!config.EnableMods)
+ return (forward, reverse.Select(p => new[]
+ {
+ p,
+ }).ToArray());
+
+ return await Task.Run(async () =>
+ {
+ var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
+ var forwardTask = Task.Run(() =>
+ {
+ var forwardRet = new string[forward.Length];
+ Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], modManager, playerCollection));
+ return forwardRet;
+ }).ConfigureAwait(false);
+ var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false);
+ var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray();
+ return (await forwardTask, reverseResolved);
+ }).ConfigureAwait(false);
+ }
+
+ /// Resolve a path given by string for a specific collection.
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private string ResolvePath(string path, ModManager _, ModCollection collection)
+ {
+ if (!config.EnableMods)
+ return path;
+
+ var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty;
+ var ret = collection.ResolvePath(gamePath);
+ return ret?.ToString() ?? path;
+ }
+}
diff --git a/Penumbra/Api/Api/ResourceTreeApi.cs b/Penumbra/Api/Api/ResourceTreeApi.cs
new file mode 100644
index 00000000..6e9aaa48
--- /dev/null
+++ b/Penumbra/Api/Api/ResourceTreeApi.cs
@@ -0,0 +1,63 @@
+using Dalamud.Game.ClientState.Objects.Types;
+using Newtonsoft.Json.Linq;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.GameData.Interop;
+using Penumbra.Interop.ResourceTree;
+
+namespace Penumbra.Api.Api;
+
+public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectManager objects) : IPenumbraApiResourceTree, IApiService
+{
+ public Dictionary>?[] GetGameObjectResourcePaths(params ushort[] gameObjects)
+ {
+ var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
+ var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0);
+ var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
+
+ return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj));
+ }
+
+ public Dictionary>> GetPlayerResourcePaths()
+ {
+ var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly);
+ return ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
+ }
+
+ public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData,
+ params ushort[] gameObjects)
+ {
+ var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
+ var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
+ var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
+
+ return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj));
+ }
+
+ public Dictionary GetPlayerResourcesOfType(ResourceType type,
+ bool withUiData)
+ {
+ var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
+ | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
+ return ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type);
+ }
+
+ public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects)
+ {
+ var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType();
+ var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0);
+ var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
+
+ return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj));
+ }
+
+ public Dictionary GetPlayerResourceTrees(bool withUiData)
+ {
+ var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
+ | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
+ var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
+
+ return resDictionary;
+ }
+}
diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs
new file mode 100644
index 00000000..b4ffa8f4
--- /dev/null
+++ b/Penumbra/Api/Api/TemporaryApi.cs
@@ -0,0 +1,190 @@
+using OtterGui;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Collections.Manager;
+using Penumbra.GameData.Actors;
+using Penumbra.GameData.Interop;
+using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Subclasses;
+using Penumbra.String.Classes;
+
+namespace Penumbra.Api.Api;
+
+public class TemporaryApi(
+ TempCollectionManager tempCollections,
+ ObjectManager objects,
+ ActorManager actors,
+ CollectionManager collectionManager,
+ TempModManager tempMods) : IPenumbraApiTemporary, IApiService
+{
+ public Guid CreateTemporaryCollection(string name)
+ => tempCollections.CreateTemporaryCollection(name);
+
+ public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
+ => tempCollections.RemoveTemporaryCollection(collectionId)
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.CollectionMissing;
+
+ public PenumbraApiEc AssignTemporaryCollection(Guid collectionId, int actorIndex, bool forceAssignment)
+ {
+ var args = ApiHelpers.Args("CollectionId", collectionId, "ActorIndex", actorIndex, "Forced", forceAssignment);
+ if (actorIndex < 0 || actorIndex >= objects.TotalCount)
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ var identifier = actors.FromObject(objects[actorIndex], out _, false, false, true);
+ if (!identifier.IsValid)
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ if (!tempCollections.CollectionById(collectionId, out var collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ if (forceAssignment)
+ {
+ if (tempCollections.Collections.ContainsKey(identifier) && !tempCollections.Collections.Delete(identifier))
+ return ApiHelpers.Return(PenumbraApiEc.AssignmentDeletionFailed, args);
+ }
+ else if (tempCollections.Collections.ContainsKey(identifier)
+ || collectionManager.Active.Individuals.ContainsKey(identifier))
+ {
+ return ApiHelpers.Return(PenumbraApiEc.CharacterCollectionExists, args);
+ }
+
+ var group = tempCollections.Collections.GetGroup(identifier);
+ var ret = tempCollections.AddIdentifier(collection, group)
+ ? PenumbraApiEc.Success
+ : PenumbraApiEc.UnknownError;
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority)
+ {
+ var args = ApiHelpers.Args("Tag", tag, "#Paths", paths.Count, "ManipString", manipString, "Priority", priority);
+ if (!ConvertPaths(paths, out var p))
+ return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
+
+ if (!ConvertManips(manipString, out var m))
+ return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
+
+ var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
+ {
+ RedirectResult.Success => PenumbraApiEc.Success,
+ _ => PenumbraApiEc.UnknownError,
+ };
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc AddTemporaryMod(string tag, Guid collectionId, Dictionary paths, string manipString, int priority)
+ {
+ var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "#Paths", paths.Count, "ManipString",
+ manipString, "Priority", priority);
+
+ if (collectionId == Guid.Empty)
+ return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
+
+ if (!tempCollections.CollectionById(collectionId, out var collection)
+ && !collectionManager.Storage.ById(collectionId, out collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ if (!ConvertPaths(paths, out var p))
+ return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
+
+ if (!ConvertManips(manipString, out var m))
+ return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
+
+ var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
+ {
+ RedirectResult.Success => PenumbraApiEc.Success,
+ _ => PenumbraApiEc.UnknownError,
+ };
+ return ApiHelpers.Return(ret, args);
+ }
+
+ public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority)
+ {
+ var ret = tempMods.Unregister(tag, null, new ModPriority(priority)) switch
+ {
+ RedirectResult.Success => PenumbraApiEc.Success,
+ RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
+ _ => PenumbraApiEc.UnknownError,
+ };
+ return ApiHelpers.Return(ret, ApiHelpers.Args("Tag", tag, "Priority", priority));
+ }
+
+ public PenumbraApiEc RemoveTemporaryMod(string tag, Guid collectionId, int priority)
+ {
+ var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "Priority", priority);
+
+ if (!tempCollections.CollectionById(collectionId, out var collection)
+ && !collectionManager.Storage.ById(collectionId, out collection))
+ return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args);
+
+ var ret = tempMods.Unregister(tag, collection, new ModPriority(priority)) switch
+ {
+ RedirectResult.Success => PenumbraApiEc.Success,
+ RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
+ _ => PenumbraApiEc.UnknownError,
+ };
+ return ApiHelpers.Return(ret, args);
+ }
+
+ ///
+ /// Convert a dictionary of strings to a dictionary of game paths to full paths.
+ /// Only returns true if all paths can successfully be converted and added.
+ ///
+ private static bool ConvertPaths(IReadOnlyDictionary redirections,
+ [NotNullWhen(true)] out Dictionary? paths)
+ {
+ paths = new Dictionary(redirections.Count);
+ foreach (var (gString, fString) in redirections)
+ {
+ if (!Utf8GamePath.FromString(gString, out var path, false))
+ {
+ paths = null;
+ return false;
+ }
+
+ var fullPath = new FullPath(fString);
+ if (!paths.TryAdd(path, fullPath))
+ {
+ paths = null;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Convert manipulations from a transmitted base64 string to actual manipulations.
+ /// The empty string is treated as an empty set.
+ /// Only returns true if all conversions are successful and distinct.
+ ///
+ private static bool ConvertManips(string manipString,
+ [NotNullWhen(true)] out HashSet? manips)
+ {
+ if (manipString.Length == 0)
+ {
+ manips = [];
+ return true;
+ }
+
+ if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion)
+ {
+ manips = null;
+ return false;
+ }
+
+ manips = new HashSet(manipArray!.Length);
+ foreach (var manip in manipArray.Where(m => m.Validate()))
+ {
+ if (manips.Add(manip))
+ continue;
+
+ Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped.");
+ manips = null;
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs
new file mode 100644
index 00000000..cf3cd8f2
--- /dev/null
+++ b/Penumbra/Api/Api/UiApi.cs
@@ -0,0 +1,101 @@
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Communication;
+using Penumbra.GameData.Enums;
+using Penumbra.Mods.Manager;
+using Penumbra.Services;
+using Penumbra.UI;
+
+namespace Penumbra.Api.Api;
+
+public class UiApi : IPenumbraApiUi, IApiService, IDisposable
+{
+ private readonly CommunicatorService _communicator;
+ private readonly ConfigWindow _configWindow;
+ private readonly ModManager _modManager;
+
+ public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager)
+ {
+ _communicator = communicator;
+ _configWindow = configWindow;
+ _modManager = modManager;
+ _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
+ _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
+ }
+
+ public void Dispose()
+ {
+ _communicator.ChangedItemHover.Unsubscribe(OnChangedItemHover);
+ _communicator.ChangedItemClick.Unsubscribe(OnChangedItemClick);
+ }
+
+ public event Action? ChangedItemTooltip;
+
+ public event Action? ChangedItemClicked;
+
+ public event Action? PreSettingsTabBarDraw
+ {
+ add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default);
+ remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!);
+ }
+
+ public event Action? PreSettingsPanelDraw
+ {
+ add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default);
+ remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!);
+ }
+
+ public event Action? PostEnabledDraw
+ {
+ add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default);
+ remove => _communicator.PostEnabledDraw.Unsubscribe(value!);
+ }
+
+ public event Action? PostSettingsPanelDraw
+ {
+ add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default);
+ remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!);
+ }
+
+ public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName)
+ {
+ _configWindow.IsOpen = true;
+ if (!Enum.IsDefined(tab))
+ return PenumbraApiEc.InvalidArgument;
+
+ if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0))
+ {
+ if (_modManager.TryGetMod(modDirectory, modName, out var mod))
+ _communicator.SelectTab.Invoke(tab, mod);
+ else
+ return PenumbraApiEc.ModMissing;
+ }
+ else if (tab != TabType.None)
+ {
+ _communicator.SelectTab.Invoke(tab, null);
+ }
+
+ return PenumbraApiEc.Success;
+ }
+
+ public void CloseMainWindow()
+ => _configWindow.IsOpen = false;
+
+ private void OnChangedItemClick(MouseButton button, object? data)
+ {
+ if (ChangedItemClicked == null)
+ return;
+
+ var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
+ ChangedItemClicked.Invoke(button, type, id);
+ }
+
+ private void OnChangedItemHover(object? data)
+ {
+ if (ChangedItemTooltip == null)
+ return;
+
+ var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
+ ChangedItemTooltip.Invoke(type, id);
+ }
+}
diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs
index 0374e31a..1c2cebcc 100644
--- a/Penumbra/Api/DalamudSubstitutionProvider.cs
+++ b/Penumbra/Api/DalamudSubstitutionProvider.cs
@@ -1,4 +1,5 @@
using Dalamud.Plugin.Services;
+using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@@ -9,7 +10,7 @@ using Penumbra.String.Classes;
namespace Penumbra.Api;
-public class DalamudSubstitutionProvider : IDisposable
+public class DalamudSubstitutionProvider : IDisposable, IApiService
{
private readonly ITextureSubstitutionProvider _substitution;
private readonly ActiveCollectionData _activeCollectionData;
diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs
index e23f8b4f..859c46b4 100644
--- a/Penumbra/Api/HttpApi.cs
+++ b/Penumbra/Api/HttpApi.cs
@@ -1,11 +1,13 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
+using OtterGui.Services;
+using Penumbra.Api.Api;
using Penumbra.Api.Enums;
namespace Penumbra.Api;
-public class HttpApi : IDisposable
+public class HttpApi : IDisposable, IApiService
{
private partial class Controller : WebApiController
{
@@ -67,7 +69,7 @@ public class HttpApi : IDisposable
public partial object? GetMods()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
- return _api.GetModList();
+ return _api.Mods.GetModList();
}
public async partial Task Redraw()
@@ -75,17 +77,15 @@ public class HttpApi : IDisposable
var data = await HttpContext.GetRequestDataAsync();
Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}.");
if (data.ObjectTableIndex >= 0)
- _api.RedrawObject(data.ObjectTableIndex, data.Type);
- else if (data.Name.Length > 0)
- _api.RedrawObject(data.Name, data.Type);
+ _api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type);
else
- _api.RedrawAll(data.Type);
+ _api.Redraw.RedrawAll(data.Type);
}
public partial void RedrawAll()
{
Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered.");
- _api.RedrawAll(RedrawType.Redraw);
+ _api.Redraw.RedrawAll(RedrawType.Redraw);
}
public async partial Task ReloadMod()
@@ -95,10 +95,10 @@ public class HttpApi : IDisposable
// 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.AddMod(data.Path);
+ _api.Mods.AddMod(data.Path);
// Reload the mod by path or name, which will also remove no-longer existing mods.
- _api.ReloadMod(data.Path, data.Name);
+ _api.Mods.ReloadMod(data.Path, data.Name);
}
public async partial Task InstallMod()
@@ -106,13 +106,13 @@ public class HttpApi : IDisposable
var data = await HttpContext.GetRequestDataAsync();
Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}.");
if (data.Path.Length != 0)
- _api.InstallMod(data.Path);
+ _api.Mods.InstallMod(data.Path);
}
public partial void OpenWindow()
{
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
- _api.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
+ _api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
}
private record ModReloadData(string Path, string Name)
diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs
new file mode 100644
index 00000000..293af588
--- /dev/null
+++ b/Penumbra/Api/IpcProviders.cs
@@ -0,0 +1,118 @@
+using Dalamud.Plugin;
+using OtterGui.Services;
+using Penumbra.Api.Api;
+using Penumbra.Api.Helpers;
+
+namespace Penumbra.Api;
+
+public sealed class IpcProviders : IDisposable, IApiService
+{
+ private readonly List _providers;
+
+ private readonly EventProvider _disposedProvider;
+ private readonly EventProvider _initializedProvider;
+
+ public IpcProviders(DalamudPluginInterface pi, IPenumbraApi api)
+ {
+ _disposedProvider = IpcSubscribers.Disposed.Provider(pi);
+ _initializedProvider = IpcSubscribers.Initialized.Provider(pi);
+ _providers =
+ [
+ IpcSubscribers.GetCollections.Provider(pi, api.Collection),
+ IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection),
+ IpcSubscribers.GetCollection.Provider(pi, api.Collection),
+ IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection),
+ IpcSubscribers.SetCollection.Provider(pi, api.Collection),
+ IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection),
+
+ IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing),
+ IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing),
+
+ IpcSubscribers.GetDrawObjectInfo.Provider(pi, api.GameState),
+ IpcSubscribers.GetCutsceneParentIndex.Provider(pi, api.GameState),
+ IpcSubscribers.SetCutsceneParentIndex.Provider(pi, api.GameState),
+ IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState),
+ IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
+ IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState),
+
+ IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta),
+ IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta),
+
+ IpcSubscribers.GetModList.Provider(pi, api.Mods),
+ IpcSubscribers.InstallMod.Provider(pi, api.Mods),
+ IpcSubscribers.ReloadMod.Provider(pi, api.Mods),
+ IpcSubscribers.AddMod.Provider(pi, api.Mods),
+ IpcSubscribers.DeleteMod.Provider(pi, api.Mods),
+ IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
+ IpcSubscribers.ModAdded.Provider(pi, api.Mods),
+ IpcSubscribers.ModMoved.Provider(pi, api.Mods),
+ IpcSubscribers.GetModPath.Provider(pi, api.Mods),
+ IpcSubscribers.SetModPath.Provider(pi, api.Mods),
+
+ IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings),
+ IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings),
+ IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings),
+ IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings),
+ IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings),
+ IpcSubscribers.TrySetModSetting.Provider(pi, api.ModSettings),
+ IpcSubscribers.TrySetModSettings.Provider(pi, api.ModSettings),
+ IpcSubscribers.ModSettingChanged.Provider(pi, api.ModSettings),
+ IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings),
+
+ IpcSubscribers.ApiVersion.Provider(pi, api),
+ IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState),
+ IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState),
+ IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
+ IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
+ IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
+
+ IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
+ IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
+ IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
+
+ IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
+ IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
+ IpcSubscribers.ResolveGameObjectPath.Provider(pi, api.Resolve),
+ IpcSubscribers.ResolvePlayerPath.Provider(pi, api.Resolve),
+ IpcSubscribers.ReverseResolveGameObjectPath.Provider(pi, api.Resolve),
+ IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve),
+ IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve),
+ IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve),
+
+ IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree),
+ IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree),
+ IpcSubscribers.GetGameObjectResourcesOfType.Provider(pi, api.ResourceTree),
+ IpcSubscribers.GetPlayerResourcesOfType.Provider(pi, api.ResourceTree),
+ IpcSubscribers.GetGameObjectResourceTrees.Provider(pi, api.ResourceTree),
+ IpcSubscribers.GetPlayerResourceTrees.Provider(pi, api.ResourceTree),
+
+ IpcSubscribers.CreateTemporaryCollection.Provider(pi, api.Temporary),
+ IpcSubscribers.DeleteTemporaryCollection.Provider(pi, api.Temporary),
+ IpcSubscribers.AssignTemporaryCollection.Provider(pi, api.Temporary),
+ IpcSubscribers.AddTemporaryModAll.Provider(pi, api.Temporary),
+ IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary),
+ IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary),
+ IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary),
+
+ IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui),
+ IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui),
+ IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui),
+ IpcSubscribers.PreSettingsPanelDraw.Provider(pi, api.Ui),
+ IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui),
+ IpcSubscribers.PostSettingsPanelDraw.Provider(pi, api.Ui),
+ IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
+ IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
+ ];
+ _initializedProvider.Invoke();
+ }
+
+ public void Dispose()
+ {
+ foreach (var provider in _providers)
+ provider.Dispose();
+ _providers.Clear();
+ _initializedProvider.Dispose();
+ _disposedProvider.Invoke();
+ _disposedProvider.Dispose();
+ }
+}
diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs
deleted file mode 100644
index 898c5de3..00000000
--- a/Penumbra/Api/IpcTester.cs
+++ /dev/null
@@ -1,1762 +0,0 @@
-using Dalamud.Game.ClientState.Objects.Enums;
-using Dalamud.Interface;
-using Dalamud.Interface.Utility;
-using Dalamud.Plugin;
-using ImGuiNET;
-using OtterGui;
-using OtterGui.Raii;
-using Penumbra.Mods;
-using Dalamud.Utility;
-using Penumbra.Api.Enums;
-using Penumbra.Api.Helpers;
-using Penumbra.String;
-using Penumbra.String.Classes;
-using Penumbra.Meta.Manipulations;
-using Penumbra.Mods.Manager;
-using Penumbra.Services;
-using Penumbra.UI;
-using Penumbra.Collections.Manager;
-using Dalamud.Plugin.Services;
-using Penumbra.GameData.Structs;
-using Penumbra.GameData.Enums;
-using Penumbra.GameData.Interop;
-
-namespace Penumbra.Api;
-
-public class IpcTester : IDisposable
-{
- private readonly PenumbraIpcProviders _ipcProviders;
- private bool _subscribed = true;
-
- private readonly PluginState _pluginState;
- private readonly IpcConfiguration _ipcConfiguration;
- private readonly Ui _ui;
- private readonly Redrawing _redrawing;
- private readonly GameState _gameState;
- private readonly Resolve _resolve;
- private readonly Collections _collections;
- private readonly Meta _meta;
- private readonly Mods _mods;
- private readonly ModSettings _modSettings;
- private readonly Editing _editing;
- private readonly Temporary _temporary;
- private readonly ResourceTree _resourceTree;
-
- public IpcTester(Configuration config, DalamudPluginInterface pi, ObjectManager objects, IClientState clientState,
- PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, TempModManager tempMods,
- TempCollectionManager tempCollections, SaveService saveService)
- {
- _ipcProviders = ipcProviders;
- _pluginState = new PluginState(pi);
- _ipcConfiguration = new IpcConfiguration(pi);
- _ui = new Ui(pi);
- _redrawing = new Redrawing(pi, objects, clientState);
- _gameState = new GameState(pi);
- _resolve = new Resolve(pi);
- _collections = new Collections(pi);
- _meta = new Meta(pi);
- _mods = new Mods(pi);
- _modSettings = new ModSettings(pi);
- _editing = new Editing(pi);
- _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, config);
- _resourceTree = new ResourceTree(pi, objects);
- UnsubscribeEvents();
- }
-
- public void Draw()
- {
- try
- {
- SubscribeEvents();
- ImGui.TextUnformatted($"API Version: {_ipcProviders.Api.ApiVersion.Breaking}.{_ipcProviders.Api.ApiVersion.Feature:D4}");
- _pluginState.Draw();
- _ipcConfiguration.Draw();
- _ui.Draw();
- _redrawing.Draw();
- _gameState.Draw();
- _resolve.Draw();
- _collections.Draw();
- _meta.Draw();
- _mods.Draw();
- _modSettings.Draw();
- _editing.Draw();
- _temporary.Draw();
- _temporary.DrawCollections();
- _temporary.DrawMods();
- _resourceTree.Draw();
- }
- catch (Exception e)
- {
- Penumbra.Log.Error($"Error during IPC Tests:\n{e}");
- }
- }
-
- private void SubscribeEvents()
- {
- if (!_subscribed)
- {
- _pluginState.Initialized.Enable();
- _pluginState.Disposed.Enable();
- _pluginState.EnabledChange.Enable();
- _redrawing.Redrawn.Enable();
- _ui.PreSettingsDraw.Enable();
- _ui.PostSettingsDraw.Enable();
- _modSettings.SettingChanged.Enable();
- _gameState.CharacterBaseCreating.Enable();
- _gameState.CharacterBaseCreated.Enable();
- _ipcConfiguration.ModDirectoryChanged.Enable();
- _gameState.GameObjectResourcePathResolved.Enable();
- _mods.DeleteSubscriber.Enable();
- _mods.AddSubscriber.Enable();
- _mods.MoveSubscriber.Enable();
- _subscribed = true;
- }
- }
-
- public void UnsubscribeEvents()
- {
- if (_subscribed)
- {
- _pluginState.Initialized.Disable();
- _pluginState.Disposed.Disable();
- _pluginState.EnabledChange.Disable();
- _redrawing.Redrawn.Disable();
- _ui.PreSettingsDraw.Disable();
- _ui.PostSettingsDraw.Disable();
- _ui.Tooltip.Disable();
- _ui.Click.Disable();
- _modSettings.SettingChanged.Disable();
- _gameState.CharacterBaseCreating.Disable();
- _gameState.CharacterBaseCreated.Disable();
- _ipcConfiguration.ModDirectoryChanged.Disable();
- _gameState.GameObjectResourcePathResolved.Disable();
- _mods.DeleteSubscriber.Disable();
- _mods.AddSubscriber.Disable();
- _mods.MoveSubscriber.Disable();
- _subscribed = false;
- }
- }
-
- public void Dispose()
- {
- _pluginState.Initialized.Dispose();
- _pluginState.Disposed.Dispose();
- _pluginState.EnabledChange.Dispose();
- _redrawing.Redrawn.Dispose();
- _ui.PreSettingsDraw.Dispose();
- _ui.PostSettingsDraw.Dispose();
- _ui.Tooltip.Dispose();
- _ui.Click.Dispose();
- _modSettings.SettingChanged.Dispose();
- _gameState.CharacterBaseCreating.Dispose();
- _gameState.CharacterBaseCreated.Dispose();
- _ipcConfiguration.ModDirectoryChanged.Dispose();
- _gameState.GameObjectResourcePathResolved.Dispose();
- _mods.DeleteSubscriber.Dispose();
- _mods.AddSubscriber.Dispose();
- _mods.MoveSubscriber.Dispose();
- _subscribed = false;
- }
-
- private static void DrawIntro(string label, string info)
- {
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(label);
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(info);
- ImGui.TableNextColumn();
- }
-
-
- private class PluginState
- {
- private readonly DalamudPluginInterface _pi;
- public readonly EventSubscriber Initialized;
- public readonly EventSubscriber Disposed;
- public readonly EventSubscriber EnabledChange;
-
- private readonly List _initializedList = new();
- private readonly List _disposedList = new();
-
- private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
- private bool? _lastEnabledValue;
-
- public PluginState(DalamudPluginInterface pi)
- {
- _pi = pi;
- Initialized = Ipc.Initialized.Subscriber(pi, AddInitialized);
- Disposed = Ipc.Disposed.Subscriber(pi, AddDisposed);
- EnabledChange = Ipc.EnabledChange.Subscriber(pi, SetLastEnabled);
- }
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Plugin State");
- if (!_)
- return;
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- void DrawList(string label, string text, List list)
- {
- DrawIntro(label, text);
- if (list.Count == 0)
- {
- ImGui.TextUnformatted("Never");
- }
- else
- {
- ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture));
- if (list.Count > 1 && ImGui.IsItemHovered())
- ImGui.SetTooltip(string.Join("\n",
- list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture))));
- }
- }
-
- DrawList(Ipc.Initialized.Label, "Last Initialized", _initializedList);
- DrawList(Ipc.Disposed.Label, "Last Disposed", _disposedList);
- DrawIntro(Ipc.ApiVersions.Label, "Current Version");
- var (breaking, features) = Ipc.ApiVersions.Subscriber(_pi).Invoke();
- ImGui.TextUnformatted($"{breaking}.{features:D4}");
- DrawIntro(Ipc.GetEnabledState.Label, "Current State");
- ImGui.TextUnformatted($"{Ipc.GetEnabledState.Subscriber(_pi).Invoke()}");
- DrawIntro(Ipc.EnabledChange.Label, "Last Change");
- ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
- }
-
- private void AddInitialized()
- => _initializedList.Add(DateTimeOffset.UtcNow);
-
- private void AddDisposed()
- => _disposedList.Add(DateTimeOffset.UtcNow);
-
- private void SetLastEnabled(bool val)
- => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val);
- }
-
- private class IpcConfiguration
- {
- private readonly DalamudPluginInterface _pi;
- public readonly EventSubscriber ModDirectoryChanged;
-
- private string _currentConfiguration = string.Empty;
- private string _lastModDirectory = string.Empty;
- private bool _lastModDirectoryValid;
- private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue;
-
- public IpcConfiguration(DalamudPluginInterface pi)
- {
- _pi = pi;
- ModDirectoryChanged = Ipc.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged);
- }
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Configuration");
- if (!_)
- return;
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.GetModDirectory.Label, "Current Mod Directory");
- ImGui.TextUnformatted(Ipc.GetModDirectory.Subscriber(_pi).Invoke());
- DrawIntro(Ipc.ModDirectoryChanged.Label, "Last Mod Directory Change");
- ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue
- ? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}"
- : "None");
- DrawIntro(Ipc.GetConfiguration.Label, "Configuration");
- if (ImGui.Button("Get"))
- {
- _currentConfiguration = Ipc.GetConfiguration.Subscriber(_pi).Invoke();
- ImGui.OpenPopup("Config Popup");
- }
-
- DrawConfigPopup();
- }
-
- private void DrawConfigPopup()
- {
- ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
- using var popup = ImRaii.Popup("Config Popup");
- if (!popup)
- return;
-
- using (ImRaii.PushFont(UiBuilder.MonoFont))
- {
- ImGuiUtil.TextWrapped(_currentConfiguration);
- }
-
- if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
- ImGui.CloseCurrentPopup();
- }
-
- private void UpdateModDirectoryChanged(string path, bool valid)
- => (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now);
- }
-
- private class Ui
- {
- private readonly DalamudPluginInterface _pi;
- public readonly EventSubscriber PreSettingsDraw;
- public readonly EventSubscriber PostSettingsDraw;
- public readonly EventSubscriber Tooltip;
- public readonly EventSubscriber Click;
-
- private string _lastDrawnMod = string.Empty;
- private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue;
- private bool _subscribedToTooltip;
- private bool _subscribedToClick;
- private string _lastClicked = string.Empty;
- private string _lastHovered = string.Empty;
- private TabType _selectTab = TabType.None;
- private string _modName = string.Empty;
- private PenumbraApiEc _ec = PenumbraApiEc.Success;
-
- public Ui(DalamudPluginInterface pi)
- {
- _pi = pi;
- PreSettingsDraw = Ipc.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
- PostSettingsDraw = Ipc.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
- Tooltip = Ipc.ChangedItemTooltip.Subscriber(pi, AddedTooltip);
- Click = Ipc.ChangedItemClick.Subscriber(pi, AddedClick);
- }
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("UI");
- if (!_)
- return;
-
- using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString()))
- {
- if (combo)
- foreach (var val in Enum.GetValues())
- {
- if (ImGui.Selectable(val.ToString(), _selectTab == val))
- _selectTab = val;
- }
- }
-
- ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256);
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.PostSettingsDraw.Label, "Last Drawn Mod");
- ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None");
-
- DrawIntro(Ipc.ChangedItemTooltip.Label, "Add Tooltip");
- if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip))
- {
- if (_subscribedToTooltip)
- Tooltip.Enable();
- else
- Tooltip.Disable();
- }
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_lastHovered);
-
- DrawIntro(Ipc.ChangedItemClick.Label, "Subscribe Click");
- if (ImGui.Checkbox("##click", ref _subscribedToClick))
- {
- if (_subscribedToClick)
- Click.Enable();
- else
- Click.Disable();
- }
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_lastClicked);
- DrawIntro(Ipc.OpenMainWindow.Label, "Open Mod Window");
- if (ImGui.Button("Open##window"))
- _ec = Ipc.OpenMainWindow.Subscriber(_pi).Invoke(_selectTab, _modName, _modName);
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_ec.ToString());
-
- DrawIntro(Ipc.CloseMainWindow.Label, "Close Mod Window");
- if (ImGui.Button("Close##window"))
- Ipc.CloseMainWindow.Subscriber(_pi).Invoke();
- }
-
- private void UpdateLastDrawnMod(string name)
- => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
-
- private void AddedTooltip(ChangedItemType type, uint id)
- {
- _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
- ImGui.TextUnformatted("IPC Test Successful");
- }
-
- private void AddedClick(MouseButton button, ChangedItemType type, uint id)
- {
- _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
- }
- }
-
- private class Redrawing
- {
- private readonly DalamudPluginInterface _pi;
- private readonly IClientState _clientState;
- private readonly ObjectManager _objects;
- public readonly EventSubscriber Redrawn;
-
- private string _redrawName = string.Empty;
- private int _redrawIndex;
- private string _lastRedrawnString = "None";
-
- public Redrawing(DalamudPluginInterface pi, ObjectManager objects, IClientState clientState)
- {
- _pi = pi;
- _objects = objects;
- _clientState = clientState;
- Redrawn = Ipc.GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn);
- }
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Redrawing");
- if (!_)
- return;
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.RedrawObjectByName.Label, "Redraw by Name");
- ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
- ImGui.InputTextWithHint("##redrawName", "Name...", ref _redrawName, 32);
- ImGui.SameLine();
- if (ImGui.Button("Redraw##Name"))
- Ipc.RedrawObjectByName.Subscriber(_pi).Invoke(_redrawName, RedrawType.Redraw);
-
- DrawIntro(Ipc.RedrawObject.Label, "Redraw Player Character");
- if (ImGui.Button("Redraw##pc") && _clientState.LocalPlayer != null)
- Ipc.RedrawObject.Subscriber(_pi).Invoke(_clientState.LocalPlayer, RedrawType.Redraw);
-
- DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index");
- var tmp = _redrawIndex;
- ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
- if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount))
- _redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount);
-
- ImGui.SameLine();
- if (ImGui.Button("Redraw##Index"))
- Ipc.RedrawObjectByIndex.Subscriber(_pi).Invoke(_redrawIndex, RedrawType.Redraw);
-
- DrawIntro(Ipc.RedrawAll.Label, "Redraw All");
- if (ImGui.Button("Redraw##All"))
- Ipc.RedrawAll.Subscriber(_pi).Invoke(RedrawType.Redraw);
-
- DrawIntro(Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:");
- ImGui.TextUnformatted(_lastRedrawnString);
- }
-
- private void SetLastRedrawn(IntPtr address, int index)
- {
- if (index < 0
- || index > _objects.TotalCount
- || address == IntPtr.Zero
- || _objects[index].Address != address)
- _lastRedrawnString = "Invalid";
-
- _lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})";
- }
- }
-
- private class GameState
- {
- private readonly DalamudPluginInterface _pi;
- public readonly EventSubscriber CharacterBaseCreating;
- public readonly EventSubscriber CharacterBaseCreated;
- public readonly EventSubscriber GameObjectResourcePathResolved;
-
-
- private string _lastCreatedGameObjectName = string.Empty;
- private IntPtr _lastCreatedDrawObject = IntPtr.Zero;
- private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
- private string _lastResolvedGamePath = string.Empty;
- private string _lastResolvedFullPath = string.Empty;
- private string _lastResolvedObject = string.Empty;
- private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue;
- private string _currentDrawObjectString = string.Empty;
- private IntPtr _currentDrawObject = IntPtr.Zero;
- private int _currentCutsceneActor;
- private int _currentCutsceneParent;
- private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success;
-
- public GameState(DalamudPluginInterface pi)
- {
- _pi = pi;
- CharacterBaseCreating = Ipc.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated);
- CharacterBaseCreated = Ipc.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2);
- GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath);
- }
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Game State");
- if (!_)
- return;
-
- if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16,
- ImGuiInputTextFlags.CharsHexadecimal))
- _currentDrawObject = IntPtr.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
- out var tmp)
- ? tmp
- : IntPtr.Zero;
-
- ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0);
- ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0);
- if (_cutsceneError is not PenumbraApiEc.Success)
- {
- ImGui.SameLine();
- ImGui.TextUnformatted("Invalid Argument on last Call");
- }
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.GetDrawObjectInfo.Label, "Draw Object Info");
- if (_currentDrawObject == IntPtr.Zero)
- {
- ImGui.TextUnformatted("Invalid");
- }
- else
- {
- var (ptr, collection) = Ipc.GetDrawObjectInfo.Subscriber(_pi).Invoke(_currentDrawObject);
- ImGui.TextUnformatted(ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}");
- }
-
- DrawIntro(Ipc.GetCutsceneParentIndex.Label, "Cutscene Parent");
- ImGui.TextUnformatted(Ipc.GetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor).ToString());
-
- DrawIntro(Ipc.SetCutsceneParentIndex.Label, "Cutscene Parent");
- if (ImGui.Button("Set Parent"))
- _cutsceneError = Ipc.SetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor, _currentCutsceneParent);
-
- DrawIntro(Ipc.CreatingCharacterBase.Label, "Last Drawobject created");
- if (_lastCreatedGameObjectTime < DateTimeOffset.Now)
- ImGui.TextUnformatted(_lastCreatedDrawObject != IntPtr.Zero
- ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"
- : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}");
-
- DrawIntro(Ipc.GameObjectResourcePathResolved.Label, "Last GamePath resolved");
- if (_lastResolvedGamePathTime < DateTimeOffset.Now)
- ImGui.TextUnformatted(
- $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}");
- }
-
- private void UpdateLastCreated(IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4)
- {
- _lastCreatedGameObjectName = GetObjectName(gameObject);
- _lastCreatedGameObjectTime = DateTimeOffset.Now;
- _lastCreatedDrawObject = IntPtr.Zero;
- }
-
- private void UpdateLastCreated2(IntPtr gameObject, string _, IntPtr drawObject)
- {
- _lastCreatedGameObjectName = GetObjectName(gameObject);
- _lastCreatedGameObjectTime = DateTimeOffset.Now;
- _lastCreatedDrawObject = drawObject;
- }
-
- private void UpdateGameObjectResourcePath(IntPtr gameObject, string gamePath, string fullPath)
- {
- _lastResolvedObject = GetObjectName(gameObject);
- _lastResolvedGamePath = gamePath;
- _lastResolvedFullPath = fullPath;
- _lastResolvedGamePathTime = DateTimeOffset.Now;
- }
-
- private static unsafe string GetObjectName(IntPtr gameObject)
- {
- var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject;
- var name = obj != null ? obj->Name : null;
- return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown";
- }
- }
-
- private class Resolve(DalamudPluginInterface pi)
- {
- private string _currentResolvePath = string.Empty;
- private string _currentResolveCharacter = string.Empty;
- private string _currentReversePath = string.Empty;
- private int _currentReverseIdx;
- private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], []));
-
- public void Draw()
- {
- using var tree = ImRaii.TreeNode("Resolving");
- if (!tree)
- return;
-
- ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength);
- ImGui.InputTextWithHint("##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32);
- ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath,
- Utf8GamePath.MaxGamePathLength);
- ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0);
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.ResolveDefaultPath.Label, "Default Collection Resolve");
- if (_currentResolvePath.Length != 0)
- ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(pi).Invoke(_currentResolvePath));
-
- DrawIntro(Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve");
- if (_currentResolvePath.Length != 0)
- ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(pi).Invoke(_currentResolvePath));
-
- DrawIntro(Ipc.ResolvePlayerPath.Label, "Player Collection Resolve");
- if (_currentResolvePath.Length != 0)
- ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(pi).Invoke(_currentResolvePath));
-
- DrawIntro(Ipc.ResolveCharacterPath.Label, "Character Collection Resolve");
- if (_currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0)
- ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(pi).Invoke(_currentResolvePath, _currentResolveCharacter));
-
- DrawIntro(Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve");
- if (_currentResolvePath.Length != 0)
- ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(pi).Invoke(_currentResolvePath, _currentReverseIdx));
-
- DrawIntro(Ipc.ReverseResolvePath.Label, "Reversed Game Paths");
- if (_currentReversePath.Length > 0)
- {
- var list = Ipc.ReverseResolvePath.Subscriber(pi).Invoke(_currentReversePath, _currentResolveCharacter);
- if (list.Length > 0)
- {
- ImGui.TextUnformatted(list[0]);
- if (list.Length > 1 && ImGui.IsItemHovered())
- ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
- }
- }
-
- DrawIntro(Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)");
- if (_currentReversePath.Length > 0)
- {
- var list = Ipc.ReverseResolvePlayerPath.Subscriber(pi).Invoke(_currentReversePath);
- if (list.Length > 0)
- {
- ImGui.TextUnformatted(list[0]);
- if (list.Length > 1 && ImGui.IsItemHovered())
- ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
- }
- }
-
- DrawIntro(Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)");
- if (_currentReversePath.Length > 0)
- {
- var list = Ipc.ReverseResolveGameObjectPath.Subscriber(pi).Invoke(_currentReversePath, _currentReverseIdx);
- if (list.Length > 0)
- {
- ImGui.TextUnformatted(list[0]);
- if (list.Length > 1 && ImGui.IsItemHovered())
- ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
- }
- }
-
- var forwardArray = _currentResolvePath.Length > 0
- ? [_currentResolvePath]
- : Array.Empty();
- var reverseArray = _currentReversePath.Length > 0
- ? [_currentReversePath]
- : Array.Empty();
-
- DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)");
- if (forwardArray.Length > 0 || reverseArray.Length > 0)
- {
- var ret = Ipc.ResolvePlayerPaths.Subscriber(pi).Invoke(forwardArray, reverseArray);
- ImGui.TextUnformatted(ConvertText(ret));
- }
-
- DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)");
- if (ImGui.Button("Start"))
- _task = Ipc.ResolvePlayerPathsAsync.Subscriber(pi).Invoke(forwardArray, reverseArray);
- var hovered = ImGui.IsItemHovered();
- ImGui.SameLine();
- ImGui.AlignTextToFramePadding();
- ImGui.TextUnformatted(_task.Status.ToString());
- if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully)
- ImGui.SetTooltip(ConvertText(_task.Result));
- return;
-
- static string ConvertText((string[], string[][]) data)
- {
- var text = string.Empty;
- if (data.Item1.Length > 0)
- {
- if (data.Item2.Length > 0)
- text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}.";
- else
- text = $"Forward: {data.Item1[0]}.";
- }
- else if (data.Item2.Length > 0)
- {
- text = $"Reverse: {string.Join("; ", data.Item2[0])}.";
- }
-
- return text;
- }
- }
- }
-
- private class Collections
- {
- private readonly DalamudPluginInterface _pi;
-
- private int _objectIdx;
- private string _collectionName = string.Empty;
- private bool _allowCreation = true;
- private bool _allowDeletion = true;
- private ApiCollectionType _type = ApiCollectionType.Current;
-
- private string _characterCollectionName = string.Empty;
- private IList _collections = [];
- private string _changedItemCollection = string.Empty;
- private IReadOnlyDictionary _changedItems = new Dictionary();
- private PenumbraApiEc _returnCode = PenumbraApiEc.Success;
- private string? _oldCollection;
-
- public Collections(DalamudPluginInterface pi)
- => _pi = pi;
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Collections");
- if (!_)
- return;
-
- ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName());
- ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0);
- ImGui.InputText("Collection Name##Collections", ref _collectionName, 64);
- ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation);
- ImGui.SameLine();
- ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion);
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro("Last Return Code", _returnCode.ToString());
- if (_oldCollection != null)
- ImGui.TextUnformatted(_oldCollection.Length == 0 ? "Created" : _oldCollection);
-
- DrawIntro(Ipc.GetCurrentCollectionName.Label, "Current Collection");
- ImGui.TextUnformatted(Ipc.GetCurrentCollectionName.Subscriber(_pi).Invoke());
- DrawIntro(Ipc.GetDefaultCollectionName.Label, "Default Collection");
- ImGui.TextUnformatted(Ipc.GetDefaultCollectionName.Subscriber(_pi).Invoke());
- DrawIntro(Ipc.GetInterfaceCollectionName.Label, "Interface Collection");
- ImGui.TextUnformatted(Ipc.GetInterfaceCollectionName.Subscriber(_pi).Invoke());
- DrawIntro(Ipc.GetCharacterCollectionName.Label, "Character");
- ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
- ImGui.InputTextWithHint("##characterCollectionName", "Character Name...", ref _characterCollectionName, 64);
- var (c, s) = Ipc.GetCharacterCollectionName.Subscriber(_pi).Invoke(_characterCollectionName);
- ImGui.SameLine();
- ImGui.TextUnformatted($"{c}, {(s ? "Custom" : "Default")}");
-
- DrawIntro(Ipc.GetCollections.Label, "Collections");
- if (ImGui.Button("Get##Collections"))
- {
- _collections = Ipc.GetCollections.Subscriber(_pi).Invoke();
- ImGui.OpenPopup("Collections");
- }
-
- DrawIntro(Ipc.GetCollectionForType.Label, "Get Special Collection");
- var name = Ipc.GetCollectionForType.Subscriber(_pi).Invoke(_type);
- ImGui.TextUnformatted(name.Length == 0 ? "Unassigned" : name);
- DrawIntro(Ipc.SetCollectionForType.Label, "Set Special Collection");
- if (ImGui.Button("Set##TypeCollection"))
- (_returnCode, _oldCollection) =
- Ipc.SetCollectionForType.Subscriber(_pi).Invoke(_type, _collectionName, _allowCreation, _allowDeletion);
-
- DrawIntro(Ipc.GetCollectionForObject.Label, "Get Object Collection");
- (var valid, var individual, name) = Ipc.GetCollectionForObject.Subscriber(_pi).Invoke(_objectIdx);
- ImGui.TextUnformatted(
- $"{(valid ? "Valid" : "Invalid")} Object, {(name.Length == 0 ? "Unassigned" : name)}{(individual ? " (Individual Assignment)" : string.Empty)}");
- DrawIntro(Ipc.SetCollectionForObject.Label, "Set Object Collection");
- if (ImGui.Button("Set##ObjectCollection"))
- (_returnCode, _oldCollection) = Ipc.SetCollectionForObject.Subscriber(_pi)
- .Invoke(_objectIdx, _collectionName, _allowCreation, _allowDeletion);
-
- if (_returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty())
- _oldCollection = null;
-
- DrawIntro(Ipc.GetChangedItems.Label, "Changed Item List");
- ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
- ImGui.InputTextWithHint("##changedCollection", "Collection Name...", ref _changedItemCollection, 64);
- ImGui.SameLine();
- if (ImGui.Button("Get"))
- {
- _changedItems = Ipc.GetChangedItems.Subscriber(_pi).Invoke(_changedItemCollection);
- ImGui.OpenPopup("Changed Item List");
- }
-
- DrawChangedItemPopup();
- DrawCollectionPopup();
- }
-
- private void DrawChangedItemPopup()
- {
- ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
- using var p = ImRaii.Popup("Changed Item List");
- if (!p)
- return;
-
- foreach (var item in _changedItems)
- ImGui.TextUnformatted(item.Key);
-
- if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
- ImGui.CloseCurrentPopup();
- }
-
- private void DrawCollectionPopup()
- {
- ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
- using var p = ImRaii.Popup("Collections");
- if (!p)
- return;
-
- foreach (var collection in _collections)
- ImGui.TextUnformatted(collection);
-
- if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
- ImGui.CloseCurrentPopup();
- }
- }
-
- private class Meta
- {
- private readonly DalamudPluginInterface _pi;
-
- private string _characterName = string.Empty;
- private int _gameObjectIndex;
-
- public Meta(DalamudPluginInterface pi)
- => _pi = pi;
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Meta");
- if (!_)
- return;
-
- ImGui.InputTextWithHint("##characterName", "Character Name...", ref _characterName, 64);
- ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0);
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.GetMetaManipulations.Label, "Meta Manipulations");
- if (ImGui.Button("Copy to Clipboard"))
- {
- var base64 = Ipc.GetMetaManipulations.Subscriber(_pi).Invoke(_characterName);
- ImGui.SetClipboardText(base64);
- }
-
- DrawIntro(Ipc.GetPlayerMetaManipulations.Label, "Player Meta Manipulations");
- if (ImGui.Button("Copy to Clipboard##Player"))
- {
- var base64 = Ipc.GetPlayerMetaManipulations.Subscriber(_pi).Invoke();
- ImGui.SetClipboardText(base64);
- }
-
- DrawIntro(Ipc.GetGameObjectMetaManipulations.Label, "Game Object Manipulations");
- if (ImGui.Button("Copy to Clipboard##GameObject"))
- {
- var base64 = Ipc.GetGameObjectMetaManipulations.Subscriber(_pi).Invoke(_gameObjectIndex);
- ImGui.SetClipboardText(base64);
- }
- }
- }
-
- private class Mods
- {
- private readonly DalamudPluginInterface _pi;
-
- private string _modDirectory = string.Empty;
- private string _modName = string.Empty;
- private string _pathInput = string.Empty;
- private string _newInstallPath = string.Empty;
- private PenumbraApiEc _lastReloadEc;
- private PenumbraApiEc _lastAddEc;
- private PenumbraApiEc _lastDeleteEc;
- private PenumbraApiEc _lastSetPathEc;
- private PenumbraApiEc _lastInstallEc;
- private IList<(string, string)> _mods = new List<(string, string)>();
-
- public readonly EventSubscriber DeleteSubscriber;
- public readonly EventSubscriber AddSubscriber;
- public readonly EventSubscriber MoveSubscriber;
-
- private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch;
- private string _lastDeletedMod = string.Empty;
- private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch;
- private string _lastAddedMod = string.Empty;
- private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch;
- private string _lastMovedModFrom = string.Empty;
- private string _lastMovedModTo = string.Empty;
-
- public Mods(DalamudPluginInterface pi)
- {
- _pi = pi;
- DeleteSubscriber = Ipc.ModDeleted.Subscriber(pi, s =>
- {
- _lastDeletedModTime = DateTimeOffset.UtcNow;
- _lastDeletedMod = s;
- });
- AddSubscriber = Ipc.ModAdded.Subscriber(pi, s =>
- {
- _lastAddedModTime = DateTimeOffset.UtcNow;
- _lastAddedMod = s;
- });
- MoveSubscriber = Ipc.ModMoved.Subscriber(pi, (s1, s2) =>
- {
- _lastMovedModTime = DateTimeOffset.UtcNow;
- _lastMovedModFrom = s1;
- _lastMovedModTo = s2;
- });
- }
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Mods");
- if (!_)
- return;
-
- ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100);
- ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100);
- ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100);
- ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100);
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.GetMods.Label, "Mods");
- if (ImGui.Button("Get##Mods"))
- {
- _mods = Ipc.GetMods.Subscriber(_pi).Invoke();
- ImGui.OpenPopup("Mods");
- }
-
- DrawIntro(Ipc.ReloadMod.Label, "Reload Mod");
- if (ImGui.Button("Reload"))
- _lastReloadEc = Ipc.ReloadMod.Subscriber(_pi).Invoke(_modDirectory, _modName);
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_lastReloadEc.ToString());
-
- DrawIntro(Ipc.InstallMod.Label, "Install Mod");
- if (ImGui.Button("Install"))
- _lastInstallEc = Ipc.InstallMod.Subscriber(_pi).Invoke(_newInstallPath);
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_lastInstallEc.ToString());
-
- DrawIntro(Ipc.AddMod.Label, "Add Mod");
- if (ImGui.Button("Add"))
- _lastAddEc = Ipc.AddMod.Subscriber(_pi).Invoke(_modDirectory);
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_lastAddEc.ToString());
-
- DrawIntro(Ipc.DeleteMod.Label, "Delete Mod");
- if (ImGui.Button("Delete"))
- _lastDeleteEc = Ipc.DeleteMod.Subscriber(_pi).Invoke(_modDirectory, _modName);
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_lastDeleteEc.ToString());
-
- DrawIntro(Ipc.GetModPath.Label, "Current Path");
- var (ec, path, def) = Ipc.GetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName);
- ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")}) [{ec}]");
-
- DrawIntro(Ipc.SetModPath.Label, "Set Path");
- if (ImGui.Button("Set"))
- _lastSetPathEc = Ipc.SetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName, _pathInput);
-
- ImGui.SameLine();
- ImGui.TextUnformatted(_lastSetPathEc.ToString());
-
- DrawIntro(Ipc.ModDeleted.Label, "Last Mod Deleted");
- if (_lastDeletedModTime > DateTimeOffset.UnixEpoch)
- ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}");
-
- DrawIntro(Ipc.ModAdded.Label, "Last Mod Added");
- if (_lastAddedModTime > DateTimeOffset.UnixEpoch)
- ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}");
-
- DrawIntro(Ipc.ModMoved.Label, "Last Mod Moved");
- if (_lastMovedModTime > DateTimeOffset.UnixEpoch)
- ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}");
-
- DrawModsPopup();
- }
-
- private void DrawModsPopup()
- {
- ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
- using var p = ImRaii.Popup("Mods");
- if (!p)
- return;
-
- foreach (var (modDir, modName) in _mods)
- ImGui.TextUnformatted($"{modDir}: {modName}");
-
- if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
- ImGui.CloseCurrentPopup();
- }
- }
-
- private class ModSettings
- {
- private readonly DalamudPluginInterface _pi;
- public readonly EventSubscriber SettingChanged;
-
- private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
- private ModSettingChange _lastSettingChangeType;
- private string _lastSettingChangeCollection = string.Empty;
- private string _lastSettingChangeMod = string.Empty;
- private bool _lastSettingChangeInherited;
- private DateTimeOffset _lastSettingChange;
-
- private string _settingsModDirectory = string.Empty;
- private string _settingsModName = string.Empty;
- private string _settingsCollection = string.Empty;
- private bool _settingsAllowInheritance = true;
- private bool _settingsInherit;
- private bool _settingsEnabled;
- private int _settingsPriority;
- private IDictionary, GroupType)>? _availableSettings;
- private IDictionary>? _currentSettings;
-
- public ModSettings(DalamudPluginInterface pi)
- {
- _pi = pi;
- SettingChanged = Ipc.ModSettingChanged.Subscriber(pi, UpdateLastModSetting);
- }
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Mod Settings");
- if (!_)
- return;
-
- ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100);
- ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100);
- ImGui.InputTextWithHint("##settingsCollection", "Collection...", ref _settingsCollection, 100);
- ImGui.Checkbox("Allow Inheritance", ref _settingsAllowInheritance);
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro("Last Error", _lastSettingsError.ToString());
- DrawIntro(Ipc.ModSettingChanged.Label, "Last Mod Setting Changed");
- ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0
- ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}"
- : "None");
- DrawIntro(Ipc.GetAvailableModSettings.Label, "Get Available Settings");
- if (ImGui.Button("Get##Available"))
- {
- _availableSettings = Ipc.GetAvailableModSettings.Subscriber(_pi).Invoke(_settingsModDirectory, _settingsModName);
- _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success;
- }
-
-
- DrawIntro(Ipc.GetCurrentModSettings.Label, "Get Current Settings");
- if (ImGui.Button("Get##Current"))
- {
- var ret = Ipc.GetCurrentModSettings.Subscriber(_pi)
- .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance);
- _lastSettingsError = ret.Item1;
- if (ret.Item1 == PenumbraApiEc.Success)
- {
- _settingsEnabled = ret.Item2?.Item1 ?? false;
- _settingsInherit = ret.Item2?.Item4 ?? false;
- _settingsPriority = ret.Item2?.Item2 ?? 0;
- _currentSettings = ret.Item2?.Item3;
- }
- else
- {
- _currentSettings = null;
- }
- }
-
- DrawIntro(Ipc.TryInheritMod.Label, "Inherit Mod");
- ImGui.Checkbox("##inherit", ref _settingsInherit);
- ImGui.SameLine();
- if (ImGui.Button("Set##Inherit"))
- _lastSettingsError = Ipc.TryInheritMod.Subscriber(_pi)
- .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit);
-
- DrawIntro(Ipc.TrySetMod.Label, "Set Enabled");
- ImGui.Checkbox("##enabled", ref _settingsEnabled);
- ImGui.SameLine();
- if (ImGui.Button("Set##Enabled"))
- _lastSettingsError = Ipc.TrySetMod.Subscriber(_pi)
- .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled);
-
- DrawIntro(Ipc.TrySetModPriority.Label, "Set Priority");
- ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
- ImGui.DragInt("##Priority", ref _settingsPriority);
- ImGui.SameLine();
- if (ImGui.Button("Set##Priority"))
- _lastSettingsError = Ipc.TrySetModPriority.Subscriber(_pi)
- .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority);
-
- DrawIntro(Ipc.CopyModSettings.Label, "Copy Mod Settings");
- if (ImGui.Button("Copy Settings"))
- _lastSettingsError = Ipc.CopyModSettings.Subscriber(_pi).Invoke(_settingsCollection, _settingsModDirectory, _settingsModName);
-
- ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection.");
-
- DrawIntro(Ipc.TrySetModSetting.Label, "Set Setting(s)");
- if (_availableSettings == null)
- return;
-
- foreach (var (group, (list, type)) in _availableSettings)
- {
- using var id = ImRaii.PushId(group);
- var preview = list.Count > 0 ? list[0] : string.Empty;
- IList current;
- if (_currentSettings != null && _currentSettings.TryGetValue(group, out current!) && current.Count > 0)
- {
- preview = current[0];
- }
- else
- {
- current = new List();
- if (_currentSettings != null)
- _currentSettings[group] = current;
- }
-
- ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
- using (var c = ImRaii.Combo("##group", preview))
- {
- if (c)
- foreach (var s in list)
- {
- var contained = current.Contains(s);
- if (ImGui.Checkbox(s, ref contained))
- {
- if (contained)
- current.Add(s);
- else
- current.Remove(s);
- }
- }
- }
-
- ImGui.SameLine();
- if (ImGui.Button("Set##setting"))
- {
- if (type == GroupType.Single)
- _lastSettingsError = Ipc.TrySetModSetting.Subscriber(_pi).Invoke(_settingsCollection,
- _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[0] : string.Empty);
- else
- _lastSettingsError = Ipc.TrySetModSettings.Subscriber(_pi).Invoke(_settingsCollection,
- _settingsModDirectory, _settingsModName, group, current.ToArray());
- }
-
- ImGui.SameLine();
- ImGui.TextUnformatted(group);
- }
- }
-
- private void UpdateLastModSetting(ModSettingChange type, string collection, string mod, bool inherited)
- {
- _lastSettingChangeType = type;
- _lastSettingChangeCollection = collection;
- _lastSettingChangeMod = mod;
- _lastSettingChangeInherited = inherited;
- _lastSettingChange = DateTimeOffset.Now;
- }
- }
-
- private class Editing
- {
- private readonly DalamudPluginInterface _pi;
-
- private string _inputPath = string.Empty;
- private string _inputPath2 = string.Empty;
- private string _outputPath = string.Empty;
- private string _outputPath2 = string.Empty;
-
- private TextureType _typeSelector;
- private bool _mipMaps = true;
-
- private Task? _task1;
- private Task? _task2;
-
- public Editing(DalamudPluginInterface pi)
- => _pi = pi;
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Editing");
- if (!_)
- return;
-
- ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256);
- ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256);
- ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256);
- ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256);
- TypeCombo();
- ImGui.Checkbox("Add MipMaps", ref _mipMaps);
-
- using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 1");
- if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false }))
- _task1 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps);
- ImGui.SameLine();
- ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString());
- if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted)
- ImGui.SetTooltip(_task1.Exception?.ToString());
-
- DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 2");
- if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false }))
- _task2 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps);
- ImGui.SameLine();
- ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString());
- if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted)
- ImGui.SetTooltip(_task2.Exception?.ToString());
- }
-
- private void TypeCombo()
- {
- using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString());
- if (!combo)
- return;
-
- foreach (var value in Enum.GetValues())
- {
- if (ImGui.Selectable(value.ToString(), _typeSelector == value))
- _typeSelector = value;
- }
- }
- }
-
- private class Temporary
- {
- private readonly DalamudPluginInterface _pi;
- private readonly ModManager _modManager;
- private readonly CollectionManager _collections;
- private readonly TempModManager _tempMods;
- private readonly TempCollectionManager _tempCollections;
- private readonly SaveService _saveService;
- private readonly Configuration _config;
-
- public Temporary(DalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods,
- TempCollectionManager tempCollections, SaveService saveService, Configuration config)
- {
- _pi = pi;
- _modManager = modManager;
- _collections = collections;
- _tempMods = tempMods;
- _tempCollections = tempCollections;
- _saveService = saveService;
- _config = config;
- }
-
- public string LastCreatedCollectionName = string.Empty;
-
- private string _tempCollectionName = string.Empty;
- private string _tempCharacterName = string.Empty;
- private string _tempModName = string.Empty;
- private string _tempGamePath = "test/game/path.mtrl";
- private string _tempFilePath = "test/success.mtrl";
- private string _tempManipulation = string.Empty;
- private PenumbraApiEc _lastTempError;
- private int _tempActorIndex;
- private bool _forceOverwrite;
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Temporary");
- if (!_)
- return;
-
- ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
- ImGui.InputTextWithHint("##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32);
- ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
- ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
- ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
- ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
- ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256);
- ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
-
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro("Last Error", _lastTempError.ToString());
- DrawIntro("Last Created Collection", LastCreatedCollectionName);
- DrawIntro(Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection");
-#pragma warning disable 0612
- if (ImGui.Button("Create##Collection"))
- (_lastTempError, LastCreatedCollectionName) = Ipc.CreateTemporaryCollection.Subscriber(_pi)
- .Invoke(_tempCollectionName, _tempCharacterName, _forceOverwrite);
-
- DrawIntro(Ipc.CreateNamedTemporaryCollection.Label, "Create Named Temporary Collection");
- if (ImGui.Button("Create##NamedCollection"))
- _lastTempError = Ipc.CreateNamedTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName);
-
- DrawIntro(Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character");
- if (ImGui.Button("Delete##Collection"))
- _lastTempError = Ipc.RemoveTemporaryCollection.Subscriber(_pi).Invoke(_tempCharacterName);
-#pragma warning restore 0612
- DrawIntro(Ipc.RemoveTemporaryCollectionByName.Label, "Remove Temporary Collection");
- if (ImGui.Button("Delete##NamedCollection"))
- _lastTempError = Ipc.RemoveTemporaryCollectionByName.Subscriber(_pi).Invoke(_tempCollectionName);
-
- DrawIntro(Ipc.AssignTemporaryCollection.Label, "Assign Temporary Collection");
- if (ImGui.Button("Assign##NamedCollection"))
- _lastTempError = Ipc.AssignTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName, _tempActorIndex, _forceOverwrite);
-
- DrawIntro(Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection");
- if (ImGui.Button("Add##Mod"))
- _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName,
- new Dictionary { { _tempGamePath, _tempFilePath } },
- _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
-
- DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection");
- if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
- "Copies the effective list from the collection named in Temporary Mod Name...",
- !_collections.Storage.ByName(_tempModName, out var copyCollection))
- && copyCollection is { HasCache: true })
- {
- var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
- var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(),
- MetaManipulation.CurrentVersion);
- _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, files, manips, 999);
- }
-
- DrawIntro(Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections");
- if (ImGui.Button("Add##All"))
- _lastTempError = Ipc.AddTemporaryModAll.Subscriber(_pi).Invoke(_tempModName,
- new Dictionary { { _tempGamePath, _tempFilePath } },
- _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
-
- DrawIntro(Ipc.RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection");
- if (ImGui.Button("Remove##Mod"))
- _lastTempError = Ipc.RemoveTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, int.MaxValue);
-
- DrawIntro(Ipc.RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections");
- if (ImGui.Button("Remove##ModAll"))
- _lastTempError = Ipc.RemoveTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, int.MaxValue);
- }
-
- public void DrawCollections()
- {
- using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
- if (!collTree)
- return;
-
- using var table = ImRaii.Table("##collTree", 5);
- if (!table)
- return;
-
- foreach (var collection in _tempCollections.Values)
- {
- ImGui.TableNextColumn();
- var character = _tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
- .FirstOrDefault()
- ?? "Unknown";
- if (ImGui.Button($"Save##{collection.Name}"))
- TemporaryMod.SaveTempCollection(_config, _saveService, _modManager, collection, character);
-
- ImGuiUtil.DrawTableColumn(collection.Name);
- ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
- ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
- ImGuiUtil.DrawTableColumn(string.Join(", ",
- _tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
- }
- }
-
- public void DrawMods()
- {
- using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods");
- if (!modTree)
- return;
-
- using var table = ImRaii.Table("##modTree", 5);
-
- void PrintList(string collectionName, IReadOnlyList list)
- {
- foreach (var mod in list)
- {
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(mod.Name);
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(mod.Priority.ToString());
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(collectionName);
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(mod.Default.Files.Count.ToString());
- if (ImGui.IsItemHovered())
- {
- using var tt = ImRaii.Tooltip();
- foreach (var (path, file) in mod.Default.Files)
- ImGui.TextUnformatted($"{path} -> {file}");
- }
-
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(mod.TotalManipulations.ToString());
- if (ImGui.IsItemHovered())
- {
- using var tt = ImRaii.Tooltip();
- foreach (var manip in mod.Default.Manipulations)
- ImGui.TextUnformatted(manip.ToString());
- }
- }
- }
-
- if (table)
- {
- PrintList("All", _tempMods.ModsForAllCollections);
- foreach (var (collection, list) in _tempMods.Mods)
- PrintList(collection.Name, list);
- }
- }
- }
-
- private class ResourceTree(DalamudPluginInterface pi, ObjectManager objects)
- {
- private readonly Stopwatch _stopwatch = new();
-
- private string _gameObjectIndices = "0";
- private ResourceType _type = ResourceType.Mtrl;
- private bool _withUiData;
-
- private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcePaths;
- private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths;
- private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType;
- private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType;
- private (string, Ipc.ResourceTree?)[]? _lastGameObjectResourceTrees;
- private (string, Ipc.ResourceTree)[]? _lastPlayerResourceTrees;
- private TimeSpan _lastCallDuration;
-
- public void Draw()
- {
- using var _ = ImRaii.TreeNode("Resource Tree");
- if (!_)
- return;
-
- ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511);
- ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues());
- ImGui.Checkbox("Also get names and icons", ref _withUiData);
-
- using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- DrawIntro(Ipc.GetGameObjectResourcePaths.Label, "Get GameObject resource paths");
- if (ImGui.Button("Get##GameObjectResourcePaths"))
- {
- var gameObjects = GetSelectedGameObjects();
- var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(pi);
- _stopwatch.Restart();
- var resourcePaths = subscriber.Invoke(gameObjects);
-
- _lastCallDuration = _stopwatch.Elapsed;
- _lastGameObjectResourcePaths = gameObjects
- .Select(i => GameObjectToString(i))
- .Zip(resourcePaths)
- .ToArray();
-
- ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcePaths));
- }
-
- DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths");
- if (ImGui.Button("Get##PlayerResourcePaths"))
- {
- var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(pi);
- _stopwatch.Restart();
- var resourcePaths = subscriber.Invoke();
-
- _lastCallDuration = _stopwatch.Elapsed;
- _lastPlayerResourcePaths = resourcePaths
- .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value))
- .ToArray();
-
- ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcePaths));
- }
-
- DrawIntro(Ipc.GetGameObjectResourcesOfType.Label, "Get GameObject resources of type");
- if (ImGui.Button("Get##GameObjectResourcesOfType"))
- {
- var gameObjects = GetSelectedGameObjects();
- var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(pi);
- _stopwatch.Restart();
- var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects);
-
- _lastCallDuration = _stopwatch.Elapsed;
- _lastGameObjectResourcesOfType = gameObjects
- .Select(i => GameObjectToString(i))
- .Zip(resourcesOfType)
- .ToArray();
-
- ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcesOfType));
- }
-
- DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type");
- if (ImGui.Button("Get##PlayerResourcesOfType"))
- {
- var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(pi);
- _stopwatch.Restart();
- var resourcesOfType = subscriber.Invoke(_type, _withUiData);
-
- _lastCallDuration = _stopwatch.Elapsed;
- _lastPlayerResourcesOfType = resourcesOfType
- .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value))
- .ToArray();
-
- ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType));
- }
-
- DrawIntro(Ipc.GetGameObjectResourceTrees.Label, "Get GameObject resource trees");
- if (ImGui.Button("Get##GameObjectResourceTrees"))
- {
- var gameObjects = GetSelectedGameObjects();
- var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(pi);
- _stopwatch.Restart();
- var trees = subscriber.Invoke(_withUiData, gameObjects);
-
- _lastCallDuration = _stopwatch.Elapsed;
- _lastGameObjectResourceTrees = gameObjects
- .Select(i => GameObjectToString(i))
- .Zip(trees)
- .ToArray();
-
- ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourceTrees));
- }
-
- DrawIntro(Ipc.GetPlayerResourceTrees.Label, "Get local player resource trees");
- if (ImGui.Button("Get##PlayerResourceTrees"))
- {
- var subscriber = Ipc.GetPlayerResourceTrees.Subscriber(pi);
- _stopwatch.Restart();
- var trees = subscriber.Invoke(_withUiData);
-
- _lastCallDuration = _stopwatch.Elapsed;
- _lastPlayerResourceTrees = trees
- .Select(pair => (GameObjectToString(pair.Key), pair.Value))
- .ToArray();
-
- ImGui.OpenPopup(nameof(Ipc.GetPlayerResourceTrees));
- }
-
- DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration);
- DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration);
-
- DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration);
- DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration);
-
- DrawPopup(nameof(Ipc.GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, _lastCallDuration);
- DrawPopup(nameof(Ipc.GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration);
- }
-
- private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class
- {
- ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500));
- using var popup = ImRaii.Popup(popupId);
- if (!popup)
- {
- result = null;
- return;
- }
-
- if (result == null)
- {
- ImGui.CloseCurrentPopup();
- return;
- }
-
- drawResult(result);
-
- ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms");
-
- if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
- {
- result = null;
- ImGui.CloseCurrentPopup();
- }
- }
-
- private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class
- {
- var firstSeen = new Dictionary();
- foreach (var (label, item) in result)
- {
- if (item == null)
- {
- ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose();
- continue;
- }
-
- if (firstSeen.TryGetValue(item, out var firstLabel))
- {
- ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose();
- continue;
- }
-
- firstSeen.Add(item, label);
-
- using var header = ImRaii.TreeNode(label);
- if (!header)
- continue;
-
- drawItem(item);
- }
- }
-
- private static void DrawResourcePaths((string, IReadOnlyDictionary?)[] result)
- {
- DrawWithHeaders(result, paths =>
- {
- using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f);
- ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f);
- ImGui.TableHeadersRow();
-
- foreach (var (actualPath, gamePaths) in paths)
- {
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(actualPath);
- ImGui.TableNextColumn();
- foreach (var gamePath in gamePaths)
- ImGui.TextUnformatted(gamePath);
- }
- });
- }
-
- private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result)
- {
- DrawWithHeaders(result, resources =>
- {
- using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit);
- if (!table)
- return;
-
- ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f);
- ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f);
- if (_withUiData)
- ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f);
- ImGui.TableHeadersRow();
-
- foreach (var (resourceHandle, (actualPath, name, icon)) in resources)
- {
- ImGui.TableNextColumn();
- TextUnformattedMono($"0x{resourceHandle:X}");
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(actualPath);
- if (_withUiData)
- {
- ImGui.TableNextColumn();
- TextUnformattedMono(icon.ToString());
- ImGui.SameLine();
- ImGui.TextUnformatted(name);
- }
- }
- });
- }
-
- private void DrawResourceTrees((string, Ipc.ResourceTree?)[] result)
- {
- DrawWithHeaders(result, tree =>
- {
- ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}");
-
- using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable);
- if (!table)
- return;
-
- if (_withUiData)
- {
- ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f);
- ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f);
- ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f);
- }
- else
- {
- ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f);
- }
-
- ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
- ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
- ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f);
- ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f);
- ImGui.TableHeadersRow();
-
- void DrawNode(Ipc.ResourceNode node)
- {
- ImGui.TableNextRow();
- ImGui.TableNextColumn();
- var hasChildren = node.Children.Any();
- using var treeNode = ImRaii.TreeNode(
- $"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}",
- hasChildren
- ? ImGuiTreeNodeFlags.SpanFullWidth
- : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen);
- if (_withUiData)
- {
- ImGui.TableNextColumn();
- TextUnformattedMono(node.Type.ToString());
- ImGui.TableNextColumn();
- TextUnformattedMono(node.Icon.ToString());
- }
-
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(node.GamePath ?? "Unknown");
- ImGui.TableNextColumn();
- ImGui.TextUnformatted(node.ActualPath);
- ImGui.TableNextColumn();
- TextUnformattedMono($"0x{node.ObjectAddress:X8}");
- ImGui.TableNextColumn();
- TextUnformattedMono($"0x{node.ResourceHandle:X8}");
-
- if (treeNode)
- foreach (var child in node.Children)
- DrawNode(child);
- }
-
- foreach (var node in tree.Nodes)
- DrawNode(node);
- });
- }
-
- private static void TextUnformattedMono(string text)
- {
- using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
- ImGui.TextUnformatted(text);
- }
-
- private ushort[] GetSelectedGameObjects()
- => _gameObjectIndices.Split(',')
- .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i))
- .ToArray();
-
- private unsafe string GameObjectToString(ObjectIndex gameObjectIndex)
- {
- var gameObject = objects[gameObjectIndex];
-
- return gameObject.Valid
- ? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})"
- : $"[{gameObjectIndex}] null";
- }
- }
-}
diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs
new file mode 100644
index 00000000..12314f0c
--- /dev/null
+++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs
@@ -0,0 +1,166 @@
+using Dalamud.Interface;
+using Dalamud.Interface.Utility;
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.IpcSubscribers;
+using Penumbra.Collections.Manager;
+using Penumbra.GameData.Enums;
+using ImGuiClip = OtterGui.ImGuiClip;
+
+namespace Penumbra.Api.IpcTester;
+
+public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService
+{
+ private int _objectIdx;
+ private string _collectionIdString = string.Empty;
+ private Guid? _collectionId = null;
+ private bool _allowCreation = true;
+ private bool _allowDeletion = true;
+ private ApiCollectionType _type = ApiCollectionType.Yourself;
+
+ private Dictionary _collections = [];
+ private (string, ChangedItemType, uint)[] _changedItems = [];
+ private PenumbraApiEc _returnCode = PenumbraApiEc.Success;
+ private (Guid Id, string Name)? _oldCollection;
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Collections");
+ if (!_)
+ return;
+
+ ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName());
+ ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0);
+ ImGuiUtil.GuidInput("Collection Id##Collections", "Collection GUID...", string.Empty, ref _collectionId, ref _collectionIdString);
+ ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation);
+ ImGui.SameLine();
+ ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion);
+
+ using var table = ImRaii.Table(string.Empty, 4, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro("Last Return Code", _returnCode.ToString());
+ if (_oldCollection != null)
+ ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString());
+
+ IpcTester.DrawIntro(GetCollection.Label, "Current Collection");
+ DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current));
+
+ IpcTester.DrawIntro(GetCollection.Label, "Default Collection");
+ DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Default));
+
+ IpcTester.DrawIntro(GetCollection.Label, "Interface Collection");
+ DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Interface));
+
+ IpcTester.DrawIntro(GetCollection.Label, "Special Collection");
+ DrawCollection(new GetCollection(pi).Invoke(_type));
+
+ IpcTester.DrawIntro(GetCollections.Label, "Collections");
+ DrawCollectionPopup();
+ if (ImGui.Button("Get##Collections"))
+ {
+ _collections = new GetCollections(pi).Invoke();
+ ImGui.OpenPopup("Collections");
+ }
+
+ IpcTester.DrawIntro(GetCollectionForObject.Label, "Get Object Collection");
+ var (valid, individual, effectiveCollection) = new GetCollectionForObject(pi).Invoke(_objectIdx);
+ DrawCollection(effectiveCollection);
+ ImGui.SameLine();
+ ImGui.TextUnformatted($"({(valid ? "Valid" : "Invalid")} Object{(individual ? ", Individual Assignment)" : ")")}");
+
+ IpcTester.DrawIntro(SetCollection.Label, "Set Special Collection");
+ if (ImGui.Button("Set##SpecialCollection"))
+ (_returnCode, _oldCollection) =
+ new SetCollection(pi).Invoke(_type, _collectionId.GetValueOrDefault(Guid.Empty), _allowCreation, _allowDeletion);
+ ImGui.TableNextColumn();
+ if (ImGui.Button("Remove##SpecialCollection"))
+ (_returnCode, _oldCollection) = new SetCollection(pi).Invoke(_type, null, _allowCreation, _allowDeletion);
+
+ IpcTester.DrawIntro(SetCollectionForObject.Label, "Set Object Collection");
+ if (ImGui.Button("Set##ObjectCollection"))
+ (_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, _collectionId.GetValueOrDefault(Guid.Empty),
+ _allowCreation, _allowDeletion);
+ ImGui.TableNextColumn();
+ if (ImGui.Button("Remove##ObjectCollection"))
+ (_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, null, _allowCreation, _allowDeletion);
+
+ IpcTester.DrawIntro(GetChangedItemsForCollection.Label, "Changed Item List");
+ DrawChangedItemPopup();
+ if (ImGui.Button("Get##ChangedItems"))
+ {
+ var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
+ _changedItems = items.Select(kvp =>
+ {
+ var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value);
+ return (kvp.Key, type, id);
+ }).ToArray();
+ ImGui.OpenPopup("Changed Item List");
+ }
+ }
+
+ private void DrawChangedItemPopup()
+ {
+ ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
+ using var p = ImRaii.Popup("Changed Item List");
+ if (!p)
+ return;
+
+ using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
+ {
+ if (t)
+ ImGuiClip.ClippedDraw(_changedItems, t =>
+ {
+ ImGuiUtil.DrawTableColumn(t.Item1);
+ ImGuiUtil.DrawTableColumn(t.Item2.ToString());
+ ImGuiUtil.DrawTableColumn(t.Item3.ToString());
+ }, ImGui.GetTextLineHeightWithSpacing());
+ }
+
+ if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
+ ImGui.CloseCurrentPopup();
+ }
+
+ private void DrawCollectionPopup()
+ {
+ ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
+ using var p = ImRaii.Popup("Collections");
+ if (!p)
+ return;
+
+ using (var t = ImRaii.Table("collections", 2, ImGuiTableFlags.SizingFixedFit))
+ {
+ if (t)
+ foreach (var collection in _collections)
+ {
+ ImGui.TableNextColumn();
+ DrawCollection((collection.Key, collection.Value));
+ }
+ }
+
+ if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
+ ImGui.CloseCurrentPopup();
+ }
+
+ private static void DrawCollection((Guid Id, string Name)? collection)
+ {
+ if (collection == null)
+ {
+ ImGui.TextUnformatted("");
+ ImGui.TableNextColumn();
+ return;
+ }
+
+ ImGui.TextUnformatted(collection.Value.Name);
+ ImGui.TableNextColumn();
+ using (ImRaii.PushFont(UiBuilder.MonoFont))
+ {
+ ImGuiUtil.CopyOnClickSelectable(collection.Value.Id.ToString());
+ }
+ }
+}
diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs
new file mode 100644
index 00000000..94b1e4e8
--- /dev/null
+++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs
@@ -0,0 +1,70 @@
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.IpcSubscribers;
+
+namespace Penumbra.Api.IpcTester;
+
+public class EditingIpcTester(DalamudPluginInterface pi) : IUiService
+{
+ private string _inputPath = string.Empty;
+ private string _inputPath2 = string.Empty;
+ private string _outputPath = string.Empty;
+ private string _outputPath2 = string.Empty;
+
+ private TextureType _typeSelector;
+ private bool _mipMaps = true;
+
+ private Task? _task1;
+ private Task? _task2;
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Editing");
+ if (!_)
+ return;
+
+ ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256);
+ ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256);
+ ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256);
+ ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256);
+ TypeCombo();
+ ImGui.Checkbox("Add MipMaps", ref _mipMaps);
+
+ using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 1");
+ if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false }))
+ _task1 = new ConvertTextureFile(pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps);
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString());
+ if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted)
+ ImGui.SetTooltip(_task1.Exception?.ToString());
+
+ IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 2");
+ if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false }))
+ _task2 = new ConvertTextureFile(pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps);
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString());
+ if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted)
+ ImGui.SetTooltip(_task2.Exception?.ToString());
+ }
+
+ private void TypeCombo()
+ {
+ using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString());
+ if (!combo)
+ return;
+
+ foreach (var value in Enum.GetValues())
+ {
+ if (ImGui.Selectable(value.ToString(), _typeSelector == value))
+ _typeSelector = value;
+ }
+ }
+}
diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs
new file mode 100644
index 00000000..2c41b882
--- /dev/null
+++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs
@@ -0,0 +1,137 @@
+using Dalamud.Interface;
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.Api.IpcSubscribers;
+using Penumbra.String;
+
+namespace Penumbra.Api.IpcTester;
+
+public class GameStateIpcTester : IUiService, IDisposable
+{
+ private readonly DalamudPluginInterface _pi;
+ public readonly EventSubscriber CharacterBaseCreating;
+ public readonly EventSubscriber CharacterBaseCreated;
+ public readonly EventSubscriber GameObjectResourcePathResolved;
+
+ private string _lastCreatedGameObjectName = string.Empty;
+ private nint _lastCreatedDrawObject = nint.Zero;
+ private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
+ private string _lastResolvedGamePath = string.Empty;
+ private string _lastResolvedFullPath = string.Empty;
+ private string _lastResolvedObject = string.Empty;
+ private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue;
+ private string _currentDrawObjectString = string.Empty;
+ private nint _currentDrawObject = nint.Zero;
+ private int _currentCutsceneActor;
+ private int _currentCutsceneParent;
+ private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success;
+
+ public GameStateIpcTester(DalamudPluginInterface pi)
+ {
+ _pi = pi;
+ CharacterBaseCreating = CreatingCharacterBase.Subscriber(pi, UpdateLastCreated);
+ CharacterBaseCreated = CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2);
+ GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath);
+ }
+
+ public void Dispose()
+ {
+ CharacterBaseCreating.Dispose();
+ CharacterBaseCreated.Dispose();
+ GameObjectResourcePathResolved.Dispose();
+ }
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Game State");
+ if (!_)
+ return;
+
+ if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16,
+ ImGuiInputTextFlags.CharsHexadecimal))
+ _currentDrawObject = nint.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
+ out var tmp)
+ ? tmp
+ : nint.Zero;
+
+ ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0);
+ ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0);
+ if (_cutsceneError is not PenumbraApiEc.Success)
+ {
+ ImGui.SameLine();
+ ImGui.TextUnformatted("Invalid Argument on last Call");
+ }
+
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(GetDrawObjectInfo.Label, "Draw Object Info");
+ if (_currentDrawObject == nint.Zero)
+ {
+ ImGui.TextUnformatted("Invalid");
+ }
+ else
+ {
+ var (ptr, (collectionId, collectionName)) = new GetDrawObjectInfo(_pi).Invoke(_currentDrawObject);
+ ImGui.TextUnformatted(ptr == nint.Zero ? $"No Actor Associated, {collectionName}" : $"{ptr:X}, {collectionName}");
+ ImGui.SameLine();
+ using (ImRaii.PushFont(UiBuilder.MonoFont))
+ {
+ ImGui.TextUnformatted(collectionId.ToString());
+ }
+ }
+
+ IpcTester.DrawIntro(GetCutsceneParentIndex.Label, "Cutscene Parent");
+ ImGui.TextUnformatted(new GetCutsceneParentIndex(_pi).Invoke(_currentCutsceneActor).ToString());
+
+ IpcTester.DrawIntro(SetCutsceneParentIndex.Label, "Cutscene Parent");
+ if (ImGui.Button("Set Parent"))
+ _cutsceneError = new SetCutsceneParentIndex(_pi)
+ .Invoke(_currentCutsceneActor, _currentCutsceneParent);
+
+ IpcTester.DrawIntro(CreatingCharacterBase.Label, "Last Drawobject created");
+ if (_lastCreatedGameObjectTime < DateTimeOffset.Now)
+ ImGui.TextUnformatted(_lastCreatedDrawObject != nint.Zero
+ ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"
+ : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}");
+
+ IpcTester.DrawIntro(IpcSubscribers.GameObjectResourcePathResolved.Label, "Last GamePath resolved");
+ if (_lastResolvedGamePathTime < DateTimeOffset.Now)
+ ImGui.TextUnformatted(
+ $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}");
+ }
+
+ private void UpdateLastCreated(nint gameObject, Guid _, nint _2, nint _3, nint _4)
+ {
+ _lastCreatedGameObjectName = GetObjectName(gameObject);
+ _lastCreatedGameObjectTime = DateTimeOffset.Now;
+ _lastCreatedDrawObject = nint.Zero;
+ }
+
+ private void UpdateLastCreated2(nint gameObject, Guid _, nint drawObject)
+ {
+ _lastCreatedGameObjectName = GetObjectName(gameObject);
+ _lastCreatedGameObjectTime = DateTimeOffset.Now;
+ _lastCreatedDrawObject = drawObject;
+ }
+
+ private void UpdateGameObjectResourcePath(nint gameObject, string gamePath, string fullPath)
+ {
+ _lastResolvedObject = GetObjectName(gameObject);
+ _lastResolvedGamePath = gamePath;
+ _lastResolvedFullPath = fullPath;
+ _lastResolvedGamePathTime = DateTimeOffset.Now;
+ }
+
+ private static unsafe string GetObjectName(nint gameObject)
+ {
+ var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject;
+ var name = obj != null ? obj->Name : null;
+ return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown";
+ }
+}
diff --git a/Penumbra/Api/IpcTester/IpcTester.cs b/Penumbra/Api/IpcTester/IpcTester.cs
new file mode 100644
index 00000000..201e7068
--- /dev/null
+++ b/Penumbra/Api/IpcTester/IpcTester.cs
@@ -0,0 +1,133 @@
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.System.Framework;
+using ImGuiNET;
+using OtterGui.Services;
+using Penumbra.Api.Api;
+
+namespace Penumbra.Api.IpcTester;
+
+public class IpcTester(
+ IpcProviders ipcProviders,
+ IPenumbraApi api,
+ PluginStateIpcTester pluginStateIpcTester,
+ UiIpcTester uiIpcTester,
+ RedrawingIpcTester redrawingIpcTester,
+ GameStateIpcTester gameStateIpcTester,
+ ResolveIpcTester resolveIpcTester,
+ CollectionsIpcTester collectionsIpcTester,
+ MetaIpcTester metaIpcTester,
+ ModsIpcTester modsIpcTester,
+ ModSettingsIpcTester modSettingsIpcTester,
+ EditingIpcTester editingIpcTester,
+ TemporaryIpcTester temporaryIpcTester,
+ ResourceTreeIpcTester resourceTreeIpcTester,
+ IFramework framework) : IUiService
+{
+ private readonly IpcProviders _ipcProviders = ipcProviders;
+ private DateTime _lastUpdate;
+ private bool _subscribed = false;
+
+ public void Draw()
+ {
+ try
+ {
+ _lastUpdate = framework.LastUpdateUTC.AddSeconds(1);
+ Subscribe();
+
+ ImGui.TextUnformatted($"API Version: {api.ApiVersion.Breaking}.{api.ApiVersion.Feature:D4}");
+ collectionsIpcTester.Draw();
+ editingIpcTester.Draw();
+ gameStateIpcTester.Draw();
+ metaIpcTester.Draw();
+ modSettingsIpcTester.Draw();
+ modsIpcTester.Draw();
+ pluginStateIpcTester.Draw();
+ redrawingIpcTester.Draw();
+ resolveIpcTester.Draw();
+ resourceTreeIpcTester.Draw();
+ uiIpcTester.Draw();
+ temporaryIpcTester.Draw();
+ temporaryIpcTester.DrawCollections();
+ temporaryIpcTester.DrawMods();
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error($"Error during IPC Tests:\n{e}");
+ }
+ }
+
+ internal static void DrawIntro(string label, string info)
+ {
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(label);
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(info);
+ ImGui.TableNextColumn();
+ }
+
+ private void Subscribe()
+ {
+ if (_subscribed)
+ return;
+
+ Penumbra.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester.");
+ gameStateIpcTester.GameObjectResourcePathResolved.Enable();
+ gameStateIpcTester.CharacterBaseCreated.Enable();
+ gameStateIpcTester.CharacterBaseCreating.Enable();
+ modSettingsIpcTester.SettingChanged.Enable();
+ modsIpcTester.DeleteSubscriber.Enable();
+ modsIpcTester.AddSubscriber.Enable();
+ modsIpcTester.MoveSubscriber.Enable();
+ pluginStateIpcTester.ModDirectoryChanged.Enable();
+ pluginStateIpcTester.Initialized.Enable();
+ pluginStateIpcTester.Disposed.Enable();
+ pluginStateIpcTester.EnabledChange.Enable();
+ redrawingIpcTester.Redrawn.Enable();
+ uiIpcTester.PreSettingsTabBar.Enable();
+ uiIpcTester.PreSettingsPanel.Enable();
+ uiIpcTester.PostEnabled.Enable();
+ uiIpcTester.PostSettingsPanelDraw.Enable();
+ uiIpcTester.ChangedItemTooltip.Enable();
+ uiIpcTester.ChangedItemClicked.Enable();
+
+ framework.Update += CheckUnsubscribe;
+ _subscribed = true;
+ }
+
+ private void CheckUnsubscribe(IFramework framework1)
+ {
+ if (_lastUpdate > framework.LastUpdateUTC)
+ return;
+
+ Unsubscribe();
+ framework.Update -= CheckUnsubscribe;
+ }
+
+ private void Unsubscribe()
+ {
+ if (!_subscribed)
+ return;
+
+ Penumbra.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester.");
+ _subscribed = false;
+ gameStateIpcTester.GameObjectResourcePathResolved.Disable();
+ gameStateIpcTester.CharacterBaseCreated.Disable();
+ gameStateIpcTester.CharacterBaseCreating.Disable();
+ modSettingsIpcTester.SettingChanged.Disable();
+ modsIpcTester.DeleteSubscriber.Disable();
+ modsIpcTester.AddSubscriber.Disable();
+ modsIpcTester.MoveSubscriber.Disable();
+ pluginStateIpcTester.ModDirectoryChanged.Disable();
+ pluginStateIpcTester.Initialized.Disable();
+ pluginStateIpcTester.Disposed.Disable();
+ pluginStateIpcTester.EnabledChange.Disable();
+ redrawingIpcTester.Redrawn.Disable();
+ uiIpcTester.PreSettingsTabBar.Disable();
+ uiIpcTester.PreSettingsPanel.Disable();
+ uiIpcTester.PostEnabled.Disable();
+ uiIpcTester.PostSettingsPanelDraw.Disable();
+ uiIpcTester.ChangedItemTooltip.Disable();
+ uiIpcTester.ChangedItemClicked.Disable();
+ }
+}
diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs
new file mode 100644
index 00000000..3fa7de7f
--- /dev/null
+++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs
@@ -0,0 +1,38 @@
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.IpcSubscribers;
+
+namespace Penumbra.Api.IpcTester;
+
+public class MetaIpcTester(DalamudPluginInterface pi) : IUiService
+{
+ private int _gameObjectIndex;
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Meta");
+ if (!_)
+ return;
+
+ ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0);
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(GetPlayerMetaManipulations.Label, "Player Meta Manipulations");
+ if (ImGui.Button("Copy to Clipboard##Player"))
+ {
+ var base64 = new GetPlayerMetaManipulations(pi).Invoke();
+ ImGui.SetClipboardText(base64);
+ }
+
+ IpcTester.DrawIntro(GetMetaManipulations.Label, "Game Object Manipulations");
+ if (ImGui.Button("Copy to Clipboard##GameObject"))
+ {
+ var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex);
+ ImGui.SetClipboardText(base64);
+ }
+ }
+}
diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs
new file mode 100644
index 00000000..c33fcdee
--- /dev/null
+++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs
@@ -0,0 +1,181 @@
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.Api.IpcSubscribers;
+using Penumbra.UI;
+
+namespace Penumbra.Api.IpcTester;
+
+public class ModSettingsIpcTester : IUiService, IDisposable
+{
+ private readonly DalamudPluginInterface _pi;
+ public readonly EventSubscriber SettingChanged;
+
+ private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success;
+ private ModSettingChange _lastSettingChangeType;
+ private Guid _lastSettingChangeCollection = Guid.Empty;
+ private string _lastSettingChangeMod = string.Empty;
+ private bool _lastSettingChangeInherited;
+ private DateTimeOffset _lastSettingChange;
+
+ private string _settingsModDirectory = string.Empty;
+ private string _settingsModName = string.Empty;
+ private Guid? _settingsCollection;
+ private string _settingsCollectionName = string.Empty;
+ private bool _settingsIgnoreInheritance;
+ private bool _settingsInherit;
+ private bool _settingsEnabled;
+ private int _settingsPriority;
+ private IReadOnlyDictionary? _availableSettings;
+ private Dictionary>? _currentSettings;
+
+ public ModSettingsIpcTester(DalamudPluginInterface pi)
+ {
+ _pi = pi;
+ SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting);
+ }
+
+ public void Dispose()
+ {
+ SettingChanged.Dispose();
+ }
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Mod Settings");
+ if (!_)
+ return;
+
+ ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100);
+ ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100);
+ ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName);
+ ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance);
+ var collection = _settingsCollection.GetValueOrDefault(Guid.Empty);
+
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro("Last Error", _lastSettingsError.ToString());
+
+ IpcTester.DrawIntro(ModSettingChanged.Label, "Last Mod Setting Changed");
+ ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0
+ ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}"
+ : "None");
+
+ IpcTester.DrawIntro(GetAvailableModSettings.Label, "Get Available Settings");
+ if (ImGui.Button("Get##Available"))
+ {
+ _availableSettings = new GetAvailableModSettings(_pi).Invoke(_settingsModDirectory, _settingsModName);
+ _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success;
+ }
+
+ IpcTester.DrawIntro(GetCurrentModSettings.Label, "Get Current Settings");
+ if (ImGui.Button("Get##Current"))
+ {
+ var ret = new GetCurrentModSettings(_pi)
+ .Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance);
+ _lastSettingsError = ret.Item1;
+ if (ret.Item1 == PenumbraApiEc.Success)
+ {
+ _settingsEnabled = ret.Item2?.Item1 ?? false;
+ _settingsInherit = ret.Item2?.Item4 ?? true;
+ _settingsPriority = ret.Item2?.Item2 ?? 0;
+ _currentSettings = ret.Item2?.Item3;
+ }
+ else
+ {
+ _currentSettings = null;
+ }
+ }
+
+ IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod");
+ ImGui.Checkbox("##inherit", ref _settingsInherit);
+ ImGui.SameLine();
+ if (ImGui.Button("Set##Inherit"))
+ _lastSettingsError = new TryInheritMod(_pi)
+ .Invoke(collection, _settingsModDirectory, _settingsInherit, _settingsModName);
+
+ IpcTester.DrawIntro(TrySetMod.Label, "Set Enabled");
+ ImGui.Checkbox("##enabled", ref _settingsEnabled);
+ ImGui.SameLine();
+ if (ImGui.Button("Set##Enabled"))
+ _lastSettingsError = new TrySetMod(_pi)
+ .Invoke(collection, _settingsModDirectory, _settingsEnabled, _settingsModName);
+
+ IpcTester.DrawIntro(TrySetModPriority.Label, "Set Priority");
+ ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
+ ImGui.DragInt("##Priority", ref _settingsPriority);
+ ImGui.SameLine();
+ if (ImGui.Button("Set##Priority"))
+ _lastSettingsError = new TrySetModPriority(_pi)
+ .Invoke(collection, _settingsModDirectory, _settingsPriority, _settingsModName);
+
+ IpcTester.DrawIntro(CopyModSettings.Label, "Copy Mod Settings");
+ if (ImGui.Button("Copy Settings"))
+ _lastSettingsError = new CopyModSettings(_pi)
+ .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName);
+
+ ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection.");
+
+ IpcTester.DrawIntro(TrySetModSetting.Label, "Set Setting(s)");
+ if (_availableSettings == null)
+ return;
+
+ foreach (var (group, (list, type)) in _availableSettings)
+ {
+ using var id = ImRaii.PushId(group);
+ var preview = list.Length > 0 ? list[0] : string.Empty;
+ if (_currentSettings != null && _currentSettings.TryGetValue(group, out var current) && current.Count > 0)
+ {
+ preview = current[0];
+ }
+ else
+ {
+ current = [];
+ if (_currentSettings != null)
+ _currentSettings[group] = current;
+ }
+
+ ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
+ using (var c = ImRaii.Combo("##group", preview))
+ {
+ if (c)
+ foreach (var s in list)
+ {
+ var contained = current.Contains(s);
+ if (ImGui.Checkbox(s, ref contained))
+ {
+ if (contained)
+ current.Add(s);
+ else
+ current.Remove(s);
+ }
+ }
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("Set##setting"))
+ _lastSettingsError = type == GroupType.Single
+ ? new TrySetModSetting(_pi).Invoke(collection, _settingsModDirectory, group, current.Count > 0 ? current[0] : string.Empty,
+ _settingsModName)
+ : new TrySetModSettings(_pi).Invoke(collection, _settingsModDirectory, group, current.ToArray(), _settingsModName);
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(group);
+ }
+ }
+
+ private void UpdateLastModSetting(ModSettingChange type, Guid collection, string mod, bool inherited)
+ {
+ _lastSettingChangeType = type;
+ _lastSettingChangeCollection = collection;
+ _lastSettingChangeMod = mod;
+ _lastSettingChangeInherited = inherited;
+ _lastSettingChange = DateTimeOffset.Now;
+ }
+}
diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs
new file mode 100644
index 00000000..878a8214
--- /dev/null
+++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs
@@ -0,0 +1,154 @@
+using Dalamud.Interface.Utility;
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.Api.IpcSubscribers;
+
+namespace Penumbra.Api.IpcTester;
+
+public class ModsIpcTester : IUiService, IDisposable
+{
+ private readonly DalamudPluginInterface _pi;
+
+ private string _modDirectory = string.Empty;
+ private string _modName = string.Empty;
+ private string _pathInput = string.Empty;
+ private string _newInstallPath = string.Empty;
+ private PenumbraApiEc _lastReloadEc;
+ private PenumbraApiEc _lastAddEc;
+ private PenumbraApiEc _lastDeleteEc;
+ private PenumbraApiEc _lastSetPathEc;
+ private PenumbraApiEc _lastInstallEc;
+ private Dictionary _mods = [];
+
+ public readonly EventSubscriber DeleteSubscriber;
+ public readonly EventSubscriber AddSubscriber;
+ public readonly EventSubscriber MoveSubscriber;
+
+ private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch;
+ private string _lastDeletedMod = string.Empty;
+ private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch;
+ private string _lastAddedMod = string.Empty;
+ private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch;
+ private string _lastMovedModFrom = string.Empty;
+ private string _lastMovedModTo = string.Empty;
+
+ public ModsIpcTester(DalamudPluginInterface pi)
+ {
+ _pi = pi;
+ DeleteSubscriber = ModDeleted.Subscriber(pi, s =>
+ {
+ _lastDeletedModTime = DateTimeOffset.UtcNow;
+ _lastDeletedMod = s;
+ });
+ AddSubscriber = ModAdded.Subscriber(pi, s =>
+ {
+ _lastAddedModTime = DateTimeOffset.UtcNow;
+ _lastAddedMod = s;
+ });
+ MoveSubscriber = ModMoved.Subscriber(pi, (s1, s2) =>
+ {
+ _lastMovedModTime = DateTimeOffset.UtcNow;
+ _lastMovedModFrom = s1;
+ _lastMovedModTo = s2;
+ });
+ }
+
+ public void Dispose()
+ {
+ DeleteSubscriber.Dispose();
+ AddSubscriber.Dispose();
+ MoveSubscriber.Dispose();
+ }
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Mods");
+ if (!_)
+ return;
+
+ ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100);
+ ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100);
+ ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100);
+ ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100);
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(GetModList.Label, "Mods");
+ DrawModsPopup();
+ if (ImGui.Button("Get##Mods"))
+ {
+ _mods = new GetModList(_pi).Invoke();
+ ImGui.OpenPopup("Mods");
+ }
+
+ IpcTester.DrawIntro(ReloadMod.Label, "Reload Mod");
+ if (ImGui.Button("Reload"))
+ _lastReloadEc = new ReloadMod(_pi).Invoke(_modDirectory, _modName);
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_lastReloadEc.ToString());
+
+ IpcTester.DrawIntro(InstallMod.Label, "Install Mod");
+ if (ImGui.Button("Install"))
+ _lastInstallEc = new InstallMod(_pi).Invoke(_newInstallPath);
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_lastInstallEc.ToString());
+
+ IpcTester.DrawIntro(AddMod.Label, "Add Mod");
+ if (ImGui.Button("Add"))
+ _lastAddEc = new AddMod(_pi).Invoke(_modDirectory);
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_lastAddEc.ToString());
+
+ IpcTester.DrawIntro(DeleteMod.Label, "Delete Mod");
+ if (ImGui.Button("Delete"))
+ _lastDeleteEc = new DeleteMod(_pi).Invoke(_modDirectory, _modName);
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_lastDeleteEc.ToString());
+
+ 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}]");
+
+ IpcTester.DrawIntro(SetModPath.Label, "Set Path");
+ if (ImGui.Button("Set"))
+ _lastSetPathEc = new SetModPath(_pi).Invoke(_modDirectory, _pathInput, _modName);
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_lastSetPathEc.ToString());
+
+ IpcTester.DrawIntro(ModDeleted.Label, "Last Mod Deleted");
+ if (_lastDeletedModTime > DateTimeOffset.UnixEpoch)
+ ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}");
+
+ IpcTester.DrawIntro(ModAdded.Label, "Last Mod Added");
+ if (_lastAddedModTime > DateTimeOffset.UnixEpoch)
+ ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}");
+
+ IpcTester.DrawIntro(ModMoved.Label, "Last Mod Moved");
+ if (_lastMovedModTime > DateTimeOffset.UnixEpoch)
+ ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}");
+ }
+
+ private void DrawModsPopup()
+ {
+ ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
+ using var p = ImRaii.Popup("Mods");
+ if (!p)
+ return;
+
+ foreach (var (modDir, modName) in _mods)
+ ImGui.TextUnformatted($"{modDir}: {modName}");
+
+ if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
+ ImGui.CloseCurrentPopup();
+ }
+}
diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs
new file mode 100644
index 00000000..0588e5bd
--- /dev/null
+++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs
@@ -0,0 +1,132 @@
+using Dalamud.Interface;
+using Dalamud.Interface.Utility;
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Helpers;
+using Penumbra.Api.IpcSubscribers;
+
+namespace Penumbra.Api.IpcTester;
+
+public class PluginStateIpcTester : IUiService, IDisposable
+{
+ private readonly DalamudPluginInterface _pi;
+ public readonly EventSubscriber ModDirectoryChanged;
+ public readonly EventSubscriber Initialized;
+ public readonly EventSubscriber Disposed;
+ public readonly EventSubscriber EnabledChange;
+
+ private string _currentConfiguration = string.Empty;
+ private string _lastModDirectory = string.Empty;
+ private bool _lastModDirectoryValid;
+ private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue;
+
+ private readonly List _initializedList = [];
+ private readonly List _disposedList = [];
+
+ private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
+ private bool? _lastEnabledValue;
+
+ public PluginStateIpcTester(DalamudPluginInterface pi)
+ {
+ _pi = pi;
+ ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged);
+ Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized);
+ Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed);
+ EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled);
+ }
+
+ public void Dispose()
+ {
+ ModDirectoryChanged.Dispose();
+ Initialized.Dispose();
+ Disposed.Dispose();
+ EnabledChange.Dispose();
+ }
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Plugin State");
+ if (!_)
+ return;
+
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ DrawList(IpcSubscribers.Initialized.Label, "Last Initialized", _initializedList);
+ DrawList(IpcSubscribers.Disposed.Label, "Last Disposed", _disposedList);
+
+ IpcTester.DrawIntro(ApiVersion.Label, "Current Version");
+ var (breaking, features) = new ApiVersion(_pi).Invoke();
+ ImGui.TextUnformatted($"{breaking}.{features:D4}");
+
+ IpcTester.DrawIntro(GetEnabledState.Label, "Current State");
+ ImGui.TextUnformatted($"{new GetEnabledState(_pi).Invoke()}");
+
+ IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
+ ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
+
+ DrawConfigPopup();
+ IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
+ if (ImGui.Button("Get"))
+ {
+ _currentConfiguration = new GetConfiguration(_pi).Invoke();
+ ImGui.OpenPopup("Config Popup");
+ }
+
+ IpcTester.DrawIntro(GetModDirectory.Label, "Current Mod Directory");
+ ImGui.TextUnformatted(new GetModDirectory(_pi).Invoke());
+
+ IpcTester.DrawIntro(IpcSubscribers.ModDirectoryChanged.Label, "Last Mod Directory Change");
+ ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue
+ ? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}"
+ : "None");
+
+ void DrawList(string label, string text, List list)
+ {
+ IpcTester.DrawIntro(label, text);
+ if (list.Count == 0)
+ {
+ ImGui.TextUnformatted("Never");
+ }
+ else
+ {
+ ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture));
+ if (list.Count > 1 && ImGui.IsItemHovered())
+ ImGui.SetTooltip(string.Join("\n",
+ list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture))));
+ }
+ }
+ }
+
+ private void DrawConfigPopup()
+ {
+ ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
+ using var popup = ImRaii.Popup("Config Popup");
+ if (!popup)
+ return;
+
+ using (ImRaii.PushFont(UiBuilder.MonoFont))
+ {
+ ImGuiUtil.TextWrapped(_currentConfiguration);
+ }
+
+ if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
+ ImGui.CloseCurrentPopup();
+ }
+
+ private void UpdateModDirectoryChanged(string path, bool valid)
+ => (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now);
+
+ private void AddInitialized()
+ => _initializedList.Add(DateTimeOffset.UtcNow);
+
+ private void AddDisposed()
+ => _disposedList.Add(DateTimeOffset.UtcNow);
+
+ private void SetLastEnabled(bool val)
+ => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val);
+}
diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs
new file mode 100644
index 00000000..281c7ad4
--- /dev/null
+++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs
@@ -0,0 +1,72 @@
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using ImGuiNET;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.Api.IpcSubscribers;
+using Penumbra.GameData.Interop;
+using Penumbra.UI;
+
+namespace Penumbra.Api.IpcTester;
+
+public class RedrawingIpcTester : IUiService, IDisposable
+{
+ private readonly DalamudPluginInterface _pi;
+ private readonly ObjectManager _objects;
+ public readonly EventSubscriber Redrawn;
+
+ private int _redrawIndex;
+ private string _lastRedrawnString = "None";
+
+ public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects)
+ {
+ _pi = pi;
+ _objects = objects;
+ Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn);
+ }
+
+ public void Dispose()
+ {
+ Redrawn.Dispose();
+ }
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Redrawing");
+ if (!_)
+ return;
+
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(RedrawObject.Label, "Redraw by Index");
+ var tmp = _redrawIndex;
+ ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
+ if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount))
+ _redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount);
+ ImGui.SameLine();
+ if (ImGui.Button("Redraw##Index"))
+ new RedrawObject(_pi).Invoke(_redrawIndex);
+
+ IpcTester.DrawIntro(RedrawAll.Label, "Redraw All");
+ if (ImGui.Button("Redraw##All"))
+ new RedrawAll(_pi).Invoke();
+
+ IpcTester.DrawIntro(GameObjectRedrawn.Label, "Last Redrawn Object:");
+ ImGui.TextUnformatted(_lastRedrawnString);
+ }
+
+ private void SetLastRedrawn(nint address, int index)
+ {
+ if (index < 0
+ || index > _objects.TotalCount
+ || address == nint.Zero
+ || _objects[index].Address != address)
+ _lastRedrawnString = "Invalid";
+
+ _lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})";
+ }
+}
diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs
new file mode 100644
index 00000000..978ed8d6
--- /dev/null
+++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs
@@ -0,0 +1,114 @@
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.IpcSubscribers;
+using Penumbra.String.Classes;
+
+namespace Penumbra.Api.IpcTester;
+
+public class ResolveIpcTester(DalamudPluginInterface pi) : IUiService
+{
+ private string _currentResolvePath = string.Empty;
+ private string _currentReversePath = string.Empty;
+ private int _currentReverseIdx;
+ private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], []));
+
+ public void Draw()
+ {
+ using var tree = ImRaii.TreeNode("Resolving");
+ if (!tree)
+ return;
+
+ ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength);
+ ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath,
+ Utf8GamePath.MaxGamePathLength);
+ ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0);
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(ResolveDefaultPath.Label, "Default Collection Resolve");
+ if (_currentResolvePath.Length != 0)
+ ImGui.TextUnformatted(new ResolveDefaultPath(pi).Invoke(_currentResolvePath));
+
+ IpcTester.DrawIntro(ResolveInterfacePath.Label, "Interface Collection Resolve");
+ if (_currentResolvePath.Length != 0)
+ ImGui.TextUnformatted(new ResolveInterfacePath(pi).Invoke(_currentResolvePath));
+
+ IpcTester.DrawIntro(ResolvePlayerPath.Label, "Player Collection Resolve");
+ if (_currentResolvePath.Length != 0)
+ ImGui.TextUnformatted(new ResolvePlayerPath(pi).Invoke(_currentResolvePath));
+
+ IpcTester.DrawIntro(ResolveGameObjectPath.Label, "Game Object Collection Resolve");
+ if (_currentResolvePath.Length != 0)
+ ImGui.TextUnformatted(new ResolveGameObjectPath(pi).Invoke(_currentResolvePath, _currentReverseIdx));
+
+ IpcTester.DrawIntro(ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)");
+ if (_currentReversePath.Length > 0)
+ {
+ var list = new ReverseResolvePlayerPath(pi).Invoke(_currentReversePath);
+ if (list.Length > 0)
+ {
+ ImGui.TextUnformatted(list[0]);
+ if (list.Length > 1 && ImGui.IsItemHovered())
+ ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
+ }
+ }
+
+ IpcTester.DrawIntro(ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)");
+ if (_currentReversePath.Length > 0)
+ {
+ var list = new ReverseResolveGameObjectPath(pi).Invoke(_currentReversePath, _currentReverseIdx);
+ if (list.Length > 0)
+ {
+ ImGui.TextUnformatted(list[0]);
+ if (list.Length > 1 && ImGui.IsItemHovered())
+ ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
+ }
+ }
+
+ var forwardArray = _currentResolvePath.Length > 0
+ ? [_currentResolvePath]
+ : Array.Empty();
+ var reverseArray = _currentReversePath.Length > 0
+ ? [_currentReversePath]
+ : Array.Empty();
+
+ IpcTester.DrawIntro(ResolvePlayerPaths.Label, "Resolved Paths (Player)");
+ if (forwardArray.Length > 0 || reverseArray.Length > 0)
+ {
+ var ret = new ResolvePlayerPaths(pi).Invoke(forwardArray, reverseArray);
+ ImGui.TextUnformatted(ConvertText(ret));
+ }
+
+ IpcTester.DrawIntro(ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)");
+ if (ImGui.Button("Start"))
+ _task = new ResolvePlayerPathsAsync(pi).Invoke(forwardArray, reverseArray);
+ var hovered = ImGui.IsItemHovered();
+ ImGui.SameLine();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted(_task.Status.ToString());
+ if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully)
+ ImGui.SetTooltip(ConvertText(_task.Result));
+ return;
+
+ static string ConvertText((string[], string[][]) data)
+ {
+ var text = string.Empty;
+ if (data.Item1.Length > 0)
+ {
+ if (data.Item2.Length > 0)
+ text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}.";
+ else
+ text = $"Forward: {data.Item1[0]}.";
+ }
+ else if (data.Item2.Length > 0)
+ {
+ text = $"Reverse: {string.Join("; ", data.Item2[0])}.";
+ }
+
+ return text;
+ }
+ }
+}
diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs
new file mode 100644
index 00000000..1f57fc9d
--- /dev/null
+++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs
@@ -0,0 +1,349 @@
+using Dalamud.Game.ClientState.Objects.Enums;
+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.Helpers;
+using Penumbra.Api.IpcSubscribers;
+using Penumbra.GameData.Enums;
+using Penumbra.GameData.Interop;
+using Penumbra.GameData.Structs;
+
+namespace Penumbra.Api.IpcTester;
+
+public class ResourceTreeIpcTester(DalamudPluginInterface pi, ObjectManager objects) : IUiService
+{
+ private readonly Stopwatch _stopwatch = new();
+
+ private string _gameObjectIndices = "0";
+ private ResourceType _type = ResourceType.Mtrl;
+ private bool _withUiData;
+
+ private (string, Dictionary>?)[]? _lastGameObjectResourcePaths;
+ private (string, Dictionary>?)[]? _lastPlayerResourcePaths;
+ private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType;
+ private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType;
+ private (string, ResourceTreeDto?)[]? _lastGameObjectResourceTrees;
+ private (string, ResourceTreeDto)[]? _lastPlayerResourceTrees;
+ private TimeSpan _lastCallDuration;
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Resource Tree");
+ if (!_)
+ return;
+
+ ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511);
+ ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues());
+ ImGui.Checkbox("Also get names and icons", ref _withUiData);
+
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(GetGameObjectResourcePaths.Label, "Get GameObject resource paths");
+ if (ImGui.Button("Get##GameObjectResourcePaths"))
+ {
+ var gameObjects = GetSelectedGameObjects();
+ var subscriber = new GetGameObjectResourcePaths(pi);
+ _stopwatch.Restart();
+ var resourcePaths = subscriber.Invoke(gameObjects);
+
+ _lastCallDuration = _stopwatch.Elapsed;
+ _lastGameObjectResourcePaths = gameObjects
+ .Select(i => GameObjectToString(i))
+ .Zip(resourcePaths)
+ .ToArray();
+
+ ImGui.OpenPopup(nameof(GetGameObjectResourcePaths));
+ }
+
+ IpcTester.DrawIntro(GetPlayerResourcePaths.Label, "Get local player resource paths");
+ if (ImGui.Button("Get##PlayerResourcePaths"))
+ {
+ var subscriber = new GetPlayerResourcePaths(pi);
+ _stopwatch.Restart();
+ var resourcePaths = subscriber.Invoke();
+
+ _lastCallDuration = _stopwatch.Elapsed;
+ _lastPlayerResourcePaths = resourcePaths
+ .Select(pair => (GameObjectToString(pair.Key), pair.Value))
+ .ToArray()!;
+
+ ImGui.OpenPopup(nameof(GetPlayerResourcePaths));
+ }
+
+ IpcTester.DrawIntro(GetGameObjectResourcesOfType.Label, "Get GameObject resources of type");
+ if (ImGui.Button("Get##GameObjectResourcesOfType"))
+ {
+ var gameObjects = GetSelectedGameObjects();
+ var subscriber = new GetGameObjectResourcesOfType(pi);
+ _stopwatch.Restart();
+ var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects);
+
+ _lastCallDuration = _stopwatch.Elapsed;
+ _lastGameObjectResourcesOfType = gameObjects
+ .Select(i => GameObjectToString(i))
+ .Zip(resourcesOfType)
+ .ToArray();
+
+ ImGui.OpenPopup(nameof(GetGameObjectResourcesOfType));
+ }
+
+ IpcTester.DrawIntro(GetPlayerResourcesOfType.Label, "Get local player resources of type");
+ if (ImGui.Button("Get##PlayerResourcesOfType"))
+ {
+ var subscriber = new GetPlayerResourcesOfType(pi);
+ _stopwatch.Restart();
+ var resourcesOfType = subscriber.Invoke(_type, _withUiData);
+
+ _lastCallDuration = _stopwatch.Elapsed;
+ _lastPlayerResourcesOfType = resourcesOfType
+ .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value))
+ .ToArray();
+
+ ImGui.OpenPopup(nameof(GetPlayerResourcesOfType));
+ }
+
+ IpcTester.DrawIntro(GetGameObjectResourceTrees.Label, "Get GameObject resource trees");
+ if (ImGui.Button("Get##GameObjectResourceTrees"))
+ {
+ var gameObjects = GetSelectedGameObjects();
+ var subscriber = new GetGameObjectResourceTrees(pi);
+ _stopwatch.Restart();
+ var trees = subscriber.Invoke(_withUiData, gameObjects);
+
+ _lastCallDuration = _stopwatch.Elapsed;
+ _lastGameObjectResourceTrees = gameObjects
+ .Select(i => GameObjectToString(i))
+ .Zip(trees)
+ .ToArray();
+
+ ImGui.OpenPopup(nameof(GetGameObjectResourceTrees));
+ }
+
+ IpcTester.DrawIntro(GetPlayerResourceTrees.Label, "Get local player resource trees");
+ if (ImGui.Button("Get##PlayerResourceTrees"))
+ {
+ var subscriber = new GetPlayerResourceTrees(pi);
+ _stopwatch.Restart();
+ var trees = subscriber.Invoke(_withUiData);
+
+ _lastCallDuration = _stopwatch.Elapsed;
+ _lastPlayerResourceTrees = trees
+ .Select(pair => (GameObjectToString(pair.Key), pair.Value))
+ .ToArray();
+
+ ImGui.OpenPopup(nameof(GetPlayerResourceTrees));
+ }
+
+ DrawPopup(nameof(GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths,
+ _lastCallDuration);
+ DrawPopup(nameof(GetPlayerResourcePaths), ref _lastPlayerResourcePaths!, DrawResourcePaths, _lastCallDuration);
+
+ DrawPopup(nameof(GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType,
+ _lastCallDuration);
+ DrawPopup(nameof(GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType,
+ _lastCallDuration);
+
+ DrawPopup(nameof(GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees,
+ _lastCallDuration);
+ DrawPopup(nameof(GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration);
+ }
+
+ private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class
+ {
+ ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500));
+ using var popup = ImRaii.Popup(popupId);
+ if (!popup)
+ {
+ result = null;
+ return;
+ }
+
+ if (result == null)
+ {
+ ImGui.CloseCurrentPopup();
+ return;
+ }
+
+ drawResult(result);
+
+ ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms");
+
+ if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
+ {
+ result = null;
+ ImGui.CloseCurrentPopup();
+ }
+ }
+
+ private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class
+ {
+ var firstSeen = new Dictionary();
+ foreach (var (label, item) in result)
+ {
+ if (item == null)
+ {
+ ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose();
+ continue;
+ }
+
+ if (firstSeen.TryGetValue(item, out var firstLabel))
+ {
+ ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose();
+ continue;
+ }
+
+ firstSeen.Add(item, label);
+
+ using var header = ImRaii.TreeNode(label);
+ if (!header)
+ continue;
+
+ drawItem(item);
+ }
+ }
+
+ private static void DrawResourcePaths((string, Dictionary>?)[] result)
+ {
+ DrawWithHeaders(result, paths =>
+ {
+ using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f);
+ ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f);
+ ImGui.TableHeadersRow();
+
+ foreach (var (actualPath, gamePaths) in paths)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(actualPath);
+ ImGui.TableNextColumn();
+ foreach (var gamePath in gamePaths)
+ ImGui.TextUnformatted(gamePath);
+ }
+ });
+ }
+
+ private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result)
+ {
+ DrawWithHeaders(result, resources =>
+ {
+ using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f);
+ ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f);
+ if (_withUiData)
+ ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f);
+ ImGui.TableHeadersRow();
+
+ foreach (var (resourceHandle, (actualPath, name, icon)) in resources)
+ {
+ ImGui.TableNextColumn();
+ TextUnformattedMono($"0x{resourceHandle:X}");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(actualPath);
+ if (_withUiData)
+ {
+ ImGui.TableNextColumn();
+ TextUnformattedMono(icon.ToString());
+ ImGui.SameLine();
+ ImGui.TextUnformatted(name);
+ }
+ }
+ });
+ }
+
+ private void DrawResourceTrees((string, ResourceTreeDto?)[] result)
+ {
+ DrawWithHeaders(result, tree =>
+ {
+ ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}");
+
+ using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable);
+ if (!table)
+ return;
+
+ if (_withUiData)
+ {
+ ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f);
+ ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f);
+ ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f);
+ }
+ else
+ {
+ ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f);
+ }
+
+ ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
+ ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
+ ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f);
+ ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f);
+ ImGui.TableHeadersRow();
+
+ void DrawNode(ResourceNodeDto node)
+ {
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ var hasChildren = node.Children.Any();
+ using var treeNode = ImRaii.TreeNode(
+ $"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}",
+ hasChildren
+ ? ImGuiTreeNodeFlags.SpanFullWidth
+ : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen);
+ if (_withUiData)
+ {
+ ImGui.TableNextColumn();
+ TextUnformattedMono(node.Type.ToString());
+ ImGui.TableNextColumn();
+ TextUnformattedMono(node.Icon.ToString());
+ }
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(node.GamePath ?? "Unknown");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(node.ActualPath);
+ ImGui.TableNextColumn();
+ TextUnformattedMono($"0x{node.ObjectAddress:X8}");
+ ImGui.TableNextColumn();
+ TextUnformattedMono($"0x{node.ResourceHandle:X8}");
+
+ if (treeNode)
+ foreach (var child in node.Children)
+ DrawNode(child);
+ }
+
+ foreach (var node in tree.Nodes)
+ DrawNode(node);
+ });
+ }
+
+ private static void TextUnformattedMono(string text)
+ {
+ using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
+ ImGui.TextUnformatted(text);
+ }
+
+ private ushort[] GetSelectedGameObjects()
+ => _gameObjectIndices.Split(',')
+ .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i))
+ .ToArray();
+
+ private unsafe string GameObjectToString(ObjectIndex gameObjectIndex)
+ {
+ var gameObject = objects[gameObjectIndex];
+
+ return gameObject.Valid
+ ? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})"
+ : $"[{gameObjectIndex}] null";
+ }
+}
diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs
new file mode 100644
index 00000000..a8405eb2
--- /dev/null
+++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs
@@ -0,0 +1,203 @@
+using Dalamud.Interface;
+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.Meta.Manipulations;
+using Penumbra.Mods;
+using Penumbra.Mods.Manager;
+using Penumbra.Services;
+
+namespace Penumbra.Api.IpcTester;
+
+public class TemporaryIpcTester(
+ DalamudPluginInterface pi,
+ ModManager modManager,
+ CollectionManager collections,
+ TempModManager tempMods,
+ TempCollectionManager tempCollections,
+ SaveService saveService,
+ Configuration config)
+ : IUiService
+{
+ public Guid LastCreatedCollectionId = Guid.Empty;
+
+ private Guid? _tempGuid;
+ private string _tempCollectionName = string.Empty;
+ private string _tempCollectionGuidName = string.Empty;
+ private string _tempModName = string.Empty;
+ private string _tempGamePath = "test/game/path.mtrl";
+ private string _tempFilePath = "test/success.mtrl";
+ private string _tempManipulation = string.Empty;
+ private PenumbraApiEc _lastTempError;
+ private int _tempActorIndex;
+ private bool _forceOverwrite;
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("Temporary");
+ if (!_)
+ return;
+
+ ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
+ ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
+ ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
+ ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
+ ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
+ ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
+ ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256);
+ ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
+
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro("Last Error", _lastTempError.ToString());
+ ImGuiUtil.DrawTableColumn("Last Created Collection");
+ ImGui.TableNextColumn();
+ using (ImRaii.PushFont(UiBuilder.MonoFont))
+ {
+ ImGuiUtil.CopyOnClickSelectable(LastCreatedCollectionId.ToString());
+ }
+
+ IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
+ if (ImGui.Button("Create##Collection"))
+ {
+ LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName);
+ if (_tempGuid == null)
+ {
+ _tempGuid = LastCreatedCollectionId;
+ _tempCollectionGuidName = LastCreatedCollectionId.ToString();
+ }
+ }
+
+ var guid = _tempGuid.GetValueOrDefault(Guid.Empty);
+
+ IpcTester.DrawIntro(DeleteTemporaryCollection.Label, "Delete Temporary Collection");
+ if (ImGui.Button("Delete##Collection"))
+ _lastTempError = new DeleteTemporaryCollection(pi).Invoke(guid);
+ ImGui.SameLine();
+ if (ImGui.Button("Delete Last##Collection"))
+ _lastTempError = new DeleteTemporaryCollection(pi).Invoke(LastCreatedCollectionId);
+
+ IpcTester.DrawIntro(AssignTemporaryCollection.Label, "Assign Temporary Collection");
+ if (ImGui.Button("Assign##NamedCollection"))
+ _lastTempError = new AssignTemporaryCollection(pi).Invoke(guid, _tempActorIndex, _forceOverwrite);
+
+ IpcTester.DrawIntro(AddTemporaryMod.Label, "Add Temporary Mod to specific Collection");
+ if (ImGui.Button("Add##Mod"))
+ _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid,
+ new Dictionary { { _tempGamePath, _tempFilePath } },
+ _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
+
+ IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Copy Existing Collection");
+ if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
+ "Copies the effective list from the collection named in Temporary Mod Name...",
+ !collections.Storage.ByName(_tempModName, out var copyCollection))
+ && copyCollection is { HasCache: true })
+ {
+ var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
+ var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(),
+ MetaManipulation.CurrentVersion);
+ _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
+ }
+
+ IpcTester.DrawIntro(AddTemporaryModAll.Label, "Add Temporary Mod to all Collections");
+ if (ImGui.Button("Add##All"))
+ _lastTempError = new AddTemporaryModAll(pi).Invoke(_tempModName,
+ new Dictionary { { _tempGamePath, _tempFilePath } },
+ _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
+
+ IpcTester.DrawIntro(RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection");
+ if (ImGui.Button("Remove##Mod"))
+ _lastTempError = new RemoveTemporaryMod(pi).Invoke(_tempModName, guid, int.MaxValue);
+
+ IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections");
+ if (ImGui.Button("Remove##ModAll"))
+ _lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue);
+ }
+
+ public void DrawCollections()
+ {
+ using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
+ if (!collTree)
+ return;
+
+ using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ foreach (var (collection, idx) in tempCollections.Values.WithIndex())
+ {
+ using var id = ImRaii.PushId(idx);
+ ImGui.TableNextColumn();
+ var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
+ .FirstOrDefault()
+ ?? "Unknown";
+ if (ImGui.Button("Save##Collection"))
+ TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character);
+
+ using (ImRaii.PushFont(UiBuilder.MonoFont))
+ {
+ ImGui.TableNextColumn();
+ ImGuiUtil.CopyOnClickSelectable(collection.Identifier);
+ }
+
+ ImGuiUtil.DrawTableColumn(collection.Name);
+ ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
+ ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
+ ImGuiUtil.DrawTableColumn(string.Join(", ",
+ tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
+ }
+ }
+
+ public void DrawMods()
+ {
+ using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods");
+ if (!modTree)
+ return;
+
+ using var table = ImRaii.Table("##modTree", 5, ImGuiTableFlags.SizingFixedFit);
+
+ void PrintList(string collectionName, IReadOnlyList list)
+ {
+ foreach (var mod in list)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(mod.Name);
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(mod.Priority.ToString());
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(collectionName);
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(mod.Default.Files.Count.ToString());
+ if (ImGui.IsItemHovered())
+ {
+ using var tt = ImRaii.Tooltip();
+ foreach (var (path, file) in mod.Default.Files)
+ ImGui.TextUnformatted($"{path} -> {file}");
+ }
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(mod.TotalManipulations.ToString());
+ if (ImGui.IsItemHovered())
+ {
+ using var tt = ImRaii.Tooltip();
+ foreach (var manip in mod.Default.Manipulations)
+ ImGui.TextUnformatted(manip.ToString());
+ }
+ }
+ }
+
+ if (table)
+ {
+ PrintList("All", tempMods.ModsForAllCollections);
+ foreach (var (collection, list) in tempMods.Mods)
+ PrintList(collection.Name, list);
+ }
+ }
+}
diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs
new file mode 100644
index 00000000..29ddc22e
--- /dev/null
+++ b/Penumbra/Api/IpcTester/UiIpcTester.cs
@@ -0,0 +1,128 @@
+using Dalamud.Plugin;
+using ImGuiNET;
+using OtterGui.Raii;
+using OtterGui.Services;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.Api.IpcSubscribers;
+using Penumbra.Communication;
+
+namespace Penumbra.Api.IpcTester;
+
+public class UiIpcTester : IUiService, IDisposable
+{
+ private readonly DalamudPluginInterface _pi;
+ public readonly EventSubscriber PreSettingsTabBar;
+ public readonly EventSubscriber PreSettingsPanel;
+ public readonly EventSubscriber PostEnabled;
+ public readonly EventSubscriber PostSettingsPanelDraw;
+ public readonly EventSubscriber ChangedItemTooltip;
+ public readonly EventSubscriber ChangedItemClicked;
+
+ private string _lastDrawnMod = string.Empty;
+ private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue;
+ private bool _subscribedToTooltip;
+ private bool _subscribedToClick;
+ private string _lastClicked = string.Empty;
+ private string _lastHovered = string.Empty;
+ private TabType _selectTab = TabType.None;
+ private string _modName = string.Empty;
+ private PenumbraApiEc _ec = PenumbraApiEc.Success;
+
+ public UiIpcTester(DalamudPluginInterface pi)
+ {
+ _pi = pi;
+ PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod);
+ PreSettingsPanel = IpcSubscribers.PreSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod);
+ PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod);
+ PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod);
+ ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip);
+ ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick);
+ }
+
+ public void Dispose()
+ {
+ PreSettingsTabBar.Dispose();
+ PreSettingsPanel.Dispose();
+ PostEnabled.Dispose();
+ PostSettingsPanelDraw.Dispose();
+ ChangedItemTooltip.Dispose();
+ ChangedItemClicked.Dispose();
+ }
+
+ public void Draw()
+ {
+ using var _ = ImRaii.TreeNode("UI");
+ if (!_)
+ return;
+
+ using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString()))
+ {
+ if (combo)
+ foreach (var val in Enum.GetValues())
+ {
+ if (ImGui.Selectable(val.ToString(), _selectTab == val))
+ _selectTab = val;
+ }
+ }
+
+ ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256);
+ using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
+ if (!table)
+ return;
+
+ IpcTester.DrawIntro(IpcSubscribers.PostSettingsPanelDraw.Label, "Last Drawn Mod");
+ ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None");
+
+ IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip");
+ if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip))
+ {
+ if (_subscribedToTooltip)
+ ChangedItemTooltip.Enable();
+ else
+ ChangedItemTooltip.Disable();
+ }
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_lastHovered);
+
+ IpcTester.DrawIntro(IpcSubscribers.ChangedItemClicked.Label, "Subscribe Click");
+ if (ImGui.Checkbox("##click", ref _subscribedToClick))
+ {
+ if (_subscribedToClick)
+ ChangedItemClicked.Enable();
+ else
+ ChangedItemClicked.Disable();
+ }
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_lastClicked);
+ IpcTester.DrawIntro(OpenMainWindow.Label, "Open Mod Window");
+ if (ImGui.Button("Open##window"))
+ _ec = new OpenMainWindow(_pi).Invoke(_selectTab, _modName, _modName);
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_ec.ToString());
+
+ IpcTester.DrawIntro(CloseMainWindow.Label, "Close Mod Window");
+ if (ImGui.Button("Close##window"))
+ new CloseMainWindow(_pi).Invoke();
+ }
+
+ private void UpdateLastDrawnMod(string name)
+ => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
+
+ private void UpdateLastDrawnMod(string name, float _1, float _2)
+ => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
+
+ private void AddedTooltip(ChangedItemType type, uint id)
+ {
+ _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
+ ImGui.TextUnformatted("IPC Test Successful");
+ }
+
+ private void AddedClick(MouseButton button, ChangedItemType type, uint id)
+ {
+ _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}";
+ }
+}
diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs
deleted file mode 100644
index dc1e8472..00000000
--- a/Penumbra/Api/PenumbraApi.cs
+++ /dev/null
@@ -1,1374 +0,0 @@
-using Dalamud.Game.ClientState.Objects.Types;
-using Dalamud.Plugin.Services;
-using Lumina.Data;
-using Newtonsoft.Json;
-using OtterGui;
-using Penumbra.Collections;
-using Penumbra.Interop.PathResolving;
-using Penumbra.Interop.Structs;
-using Penumbra.Meta.Manipulations;
-using Penumbra.Mods;
-using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
-using OtterGui.Compression;
-using OtterGui.Log;
-using Penumbra.Api.Enums;
-using Penumbra.GameData.Actors;
-using Penumbra.Interop.ResourceLoading;
-using Penumbra.Mods.Manager;
-using Penumbra.String;
-using Penumbra.String.Classes;
-using Penumbra.Services;
-using Penumbra.Collections.Manager;
-using Penumbra.Communication;
-using Penumbra.GameData.Interop;
-using Penumbra.Import.Textures;
-using Penumbra.Interop.Services;
-using Penumbra.UI;
-using TextureType = Penumbra.Api.Enums.TextureType;
-using Penumbra.Interop.ResourceTree;
-using Penumbra.Mods.Editor;
-using Penumbra.Mods.Subclasses;
-
-namespace Penumbra.Api;
-
-public class PenumbraApi : IDisposable, IPenumbraApi
-{
- public (int, int) ApiVersion
- => (4, 24);
-
- public event Action? PreSettingsTabBarDraw
- {
- add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default);
- remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!);
- }
-
- public event Action? PreSettingsPanelDraw
- {
- add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default);
- remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!);
- }
-
- public event Action? PostEnabledDraw
- {
- add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default);
- remove => _communicator.PostEnabledDraw.Unsubscribe(value!);
- }
-
- public event Action? PostSettingsPanelDraw
- {
- add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default);
- remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!);
- }
-
- public event GameObjectRedrawnDelegate? GameObjectRedrawn
- {
- add
- {
- CheckInitialized();
- _redrawService.GameObjectRedrawn += value;
- }
- remove
- {
- CheckInitialized();
- _redrawService.GameObjectRedrawn -= value;
- }
- }
-
- public event ModSettingChangedDelegate? ModSettingChanged;
-
- public event CreatingCharacterBaseDelegate? CreatingCharacterBase
- {
- add
- {
- if (value == null)
- return;
-
- CheckInitialized();
- _communicator.CreatingCharacterBase.Subscribe(new Action(value),
- Communication.CreatingCharacterBase.Priority.Api);
- }
- remove
- {
- if (value == null)
- return;
-
- CheckInitialized();
- _communicator.CreatingCharacterBase.Unsubscribe(new Action(value));
- }
- }
-
- public event CreatedCharacterBaseDelegate? CreatedCharacterBase;
-
- public bool Valid
- => _lumina != null;
-
- private CommunicatorService _communicator;
- private Lumina.GameData? _lumina;
-
- private IDataManager _gameData;
- private IFramework _framework;
- private ObjectManager _objects;
- private ModManager _modManager;
- private ResourceLoader _resourceLoader;
- private Configuration _config;
- private CollectionManager _collectionManager;
- private TempCollectionManager _tempCollections;
- private TempModManager _tempMods;
- private ActorManager _actors;
- private CollectionResolver _collectionResolver;
- private CutsceneService _cutsceneService;
- private ModImportManager _modImportManager;
- private CollectionEditor _collectionEditor;
- private RedrawService _redrawService;
- private ModFileSystem _modFileSystem;
- private ConfigWindow _configWindow;
- private TextureManager _textureManager;
- private ResourceTreeFactory _resourceTreeFactory;
-
- public unsafe PenumbraApi(CommunicatorService communicator, IDataManager gameData, IFramework framework, ObjectManager objects,
- ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager,
- TempCollectionManager tempCollections, TempModManager tempMods, ActorManager actors, CollectionResolver collectionResolver,
- CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService,
- ModFileSystem modFileSystem, ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory)
- {
- _communicator = communicator;
- _gameData = gameData;
- _framework = framework;
- _objects = objects;
- _modManager = modManager;
- _resourceLoader = resourceLoader;
- _config = config;
- _collectionManager = collectionManager;
- _tempCollections = tempCollections;
- _tempMods = tempMods;
- _actors = actors;
- _collectionResolver = collectionResolver;
- _cutsceneService = cutsceneService;
- _modImportManager = modImportManager;
- _collectionEditor = collectionEditor;
- _redrawService = redrawService;
- _modFileSystem = modFileSystem;
- _configWindow = configWindow;
- _textureManager = textureManager;
- _resourceTreeFactory = resourceTreeFactory;
- _lumina = gameData.GameData;
-
- _resourceLoader.ResourceLoaded += OnResourceLoaded;
- _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api);
- _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api);
- _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
- _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api);
- _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api);
- }
-
- public unsafe void Dispose()
- {
- if (!Valid)
- return;
-
- _resourceLoader.ResourceLoaded -= OnResourceLoaded;
- _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber);
- _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
- _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
- _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited);
- _communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
- _lumina = null;
- _communicator = null!;
- _modManager = null!;
- _resourceLoader = null!;
- _config = null!;
- _collectionManager = null!;
- _tempCollections = null!;
- _tempMods = null!;
- _actors = null!;
- _collectionResolver = null!;
- _cutsceneService = null!;
- _modImportManager = null!;
- _collectionEditor = null!;
- _redrawService = null!;
- _modFileSystem = null!;
- _configWindow = null!;
- _textureManager = null!;
- _resourceTreeFactory = null!;
- _framework = null!;
- }
-
- public event ChangedItemClick? ChangedItemClicked
- {
- add => _communicator.ChangedItemClick.Subscribe(new Action(value!),
- Communication.ChangedItemClick.Priority.Default);
- remove => _communicator.ChangedItemClick.Unsubscribe(new Action(value!));
- }
-
- public string GetModDirectory()
- {
- CheckInitialized();
- return _config.ModDirectory;
- }
-
- private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath,
- ResolveData resolveData)
- {
- if (resolveData.AssociatedGameObject != nint.Zero)
- GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(),
- manipulatedPath?.ToString() ?? originalPath.ToString());
- }
-
- public event Action? ModDirectoryChanged
- {
- add
- {
- CheckInitialized();
- _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
- }
- remove
- {
- CheckInitialized();
- _communicator.ModDirectoryChanged.Unsubscribe(value!);
- }
- }
-
- public bool GetEnabledState()
- => _config.EnableMods;
-
- public event Action? EnabledChange
- {
- add
- {
- CheckInitialized();
- _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
- }
- remove
- {
- CheckInitialized();
- _communicator.EnabledChanged.Unsubscribe(value!);
- }
- }
-
- public string GetConfiguration()
- {
- CheckInitialized();
- return JsonConvert.SerializeObject(_config, Formatting.Indented);
- }
-
- public event ChangedItemHover? ChangedItemTooltip
- {
- add => _communicator.ChangedItemHover.Subscribe(new Action