Rework API, use Collection ID in crash handler, use collection GUIDs in more places.

This commit is contained in:
Ottermandias 2024-04-12 12:33:57 +02:00
parent 793ed4f0a7
commit ba8999914f
88 changed files with 4193 additions and 3930 deletions

@ -1 +1 @@
Subproject commit 4673e93f5165108a7f5b91236406d527f16384a5
Subproject commit 9599c806877e2972f964dfa68e5207cf3a8f2b84

@ -1 +1 @@
Subproject commit 8787efc8fc897dfbb4515ebbabbcd5e6f54d1b42
Subproject commit e5c8f5446879e2e0e541eb4d8fee15e98b1885bc

View file

@ -24,7 +24,7 @@ public record struct VfxFuncInvokedEntry(
string InvocationType,
string CharacterName,
string CharacterAddress,
string CollectionName) : ICrashDataEntry;
Guid CollectionId) : ICrashDataEntry;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface IAnimationInvocationBufferWriter
@ -32,19 +32,19 @@ public interface IAnimationInvocationBufferWriter
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
/// <param name="collectionId"> The GUID of the associated collection. </param>
/// <param name="type"> The type of VFX func called. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, AnimationInvocationType type);
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> 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<byte> characterName, string collectionName, AnimationInvocationType type)
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> 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,
};
}
}

View file

@ -8,8 +8,8 @@ public interface ICharacterBaseBufferWriter
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName);
/// <param name="collectionId"> The GUID of the associated collection. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId);
}
/// <summary> The full crash entry for a loaded character base. </summary>
@ -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<byte> characterName, string collectionName)
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> 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,
};
}
}

View file

@ -8,10 +8,10 @@ public interface IModdedFileBufferWriter
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
/// <param name="collectionId"> The GUID of the associated collection. </param>
/// <param name="requestedFileName"> The file name as requested by the game. </param>
/// <param name="actualFileName"> The actual modded file name loaded. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, ReadOnlySpan<byte> requestedFileName,
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> 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<byte> characterName, string collectionName, ReadOnlySpan<byte> requestedFileName,
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, Guid collectionId, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> 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,
};
}
}

@ -1 +1 @@
Subproject commit 45679aa32cc37b59f5eeb7cf6bf5a3ea36c626e0
Subproject commit 60222d79420662fb8e9960a66e262a380fcaf186

View file

@ -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
{
/// <summary> Return the associated identifier for an object given by its index. </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
});
}
}

View file

@ -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<Guid, string> GetCollections()
=> collections.Storage.ToDictionary(c => c.Id, c => c.Name);
public Dictionary<string, object?> 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<Guid, string>?();
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<Guid, string>?();
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);
}
}

View file

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

View file

@ -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<nint, Guid, nint, nint, nint>(value),
Communication.CreatingCharacterBase.Priority.Api);
}
remove
{
if (value == null)
return;
_communicator.CreatingCharacterBase.Unsubscribe(new Action<nint, Guid, nint, nint, nint>(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);
}

View file

@ -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);
}
}

View file

@ -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<string, (string[], int)>(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<string, (string[], int)>? 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<string, List<string>>, 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<string> 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);
}
}

132
Penumbra/Api/Api/ModsApi.cs Normal file
View file

@ -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<string, string> 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<string>? ModDeleted;
public event Action<string>? ModAdded;
public event Action<string, string>? 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;
}
}
}

View file

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

View file

@ -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<string, bool>? ModDirectoryChanged
{
add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
}
public bool GetEnabledState()
=> config.EnableMods;
public event Action<bool>? EnabledChange
{
add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
remove => communicator.EnabledChanged.Unsubscribe(value!);
}
}

View file

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

View file

@ -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);
}
/// <summary> Resolve a path given by string for a specific collection. </summary>
[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;
}
}

View file

@ -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<string, HashSet<string>>?[] GetGameObjectResourcePaths(params ushort[] gameObjects)
{
var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType<Character>();
var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0);
var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees);
return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj));
}
public Dictionary<ushort, Dictionary<string, HashSet<string>>> 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<Character>();
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<ushort, GameResourceDict> 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<Character>();
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<ushort, JObject> GetPlayerResourceTrees(bool withUiData)
{
var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly
| (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0));
var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees);
return resDictionary;
}
}

View file

@ -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<string, string> 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<string, string> 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);
}
/// <summary>
/// Convert a dictionary of strings to a dictionary of game paths to full paths.
/// Only returns true if all paths can successfully be converted and added.
/// </summary>
private static bool ConvertPaths(IReadOnlyDictionary<string, string> redirections,
[NotNullWhen(true)] out Dictionary<Utf8GamePath, FullPath>? paths)
{
paths = new Dictionary<Utf8GamePath, FullPath>(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;
}
/// <summary>
/// Convert manipulations from a transmitted base64 string to actual manipulations.
/// The empty string is treated as an empty set.
/// Only returns true if all conversions are successful and distinct.
/// </summary>
private static bool ConvertManips(string manipString,
[NotNullWhen(true)] out HashSet<MetaManipulation>? manips)
{
if (manipString.Length == 0)
{
manips = [];
return true;
}
if (Functions.FromCompressedBase64<MetaManipulation[]>(manipString, out var manipArray) != MetaManipulation.CurrentVersion)
{
manips = null;
return false;
}
manips = new HashSet<MetaManipulation>(manipArray!.Length);
foreach (var manip in manipArray.Where(m => m.Validate()))
{
if (manips.Add(manip))
continue;
Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped.");
manips = null;
return false;
}
return true;
}
}

101
Penumbra/Api/Api/UiApi.cs Normal file
View file

@ -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<ChangedItemType, uint>? ChangedItemTooltip;
public event Action<MouseButton, ChangedItemType, uint>? ChangedItemClicked;
public event Action<string, float, float>? PreSettingsTabBarDraw
{
add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default);
remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!);
}
public event Action<string>? PreSettingsPanelDraw
{
add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default);
remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!);
}
public event Action<string>? PostEnabledDraw
{
add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default);
remove => _communicator.PostEnabledDraw.Unsubscribe(value!);
}
public event Action<string>? 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);
}
}

View file

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

View file

@ -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<RedrawData>();
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<ModInstallData>();
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)

View file

@ -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<IDisposable> _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();
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<Guid, string> _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("<Unassigned>");
ImGui.TableNextColumn();
return;
}
ImGui.TextUnformatted(collection.Value.Name);
ImGui.TableNextColumn();
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGuiUtil.CopyOnClickSelectable(collection.Value.Id.ToString());
}
}
}

View file

@ -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<TextureType>())
{
if (ImGui.Selectable(value.ToString(), _typeSelector == value))
_typeSelector = value;
}
}
}

View file

@ -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<nint, Guid, nint, nint, nint> CharacterBaseCreating;
public readonly EventSubscriber<nint, Guid, nint> CharacterBaseCreated;
public readonly EventSubscriber<nint, string, string> 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";
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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<ModSettingChange, Guid, string, bool> 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<string, (string[], GroupType)>? _availableSettings;
private Dictionary<string, List<string>>? _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;
}
}

View file

@ -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<string, string> _mods = [];
public readonly EventSubscriber<string> DeleteSubscriber;
public readonly EventSubscriber<string> AddSubscriber;
public readonly EventSubscriber<string, string> 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();
}
}

View file

@ -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<string, bool> ModDirectoryChanged;
public readonly EventSubscriber Initialized;
public readonly EventSubscriber Disposed;
public readonly EventSubscriber<bool> EnabledChange;
private string _currentConfiguration = string.Empty;
private string _lastModDirectory = string.Empty;
private bool _lastModDirectoryValid;
private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue;
private readonly List<DateTimeOffset> _initializedList = [];
private readonly List<DateTimeOffset> _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<DateTimeOffset> 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);
}

View file

@ -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<nint, int> 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})";
}
}

View file

@ -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<string>();
var reverseArray = _currentReversePath.Length > 0
? [_currentReversePath]
: Array.Empty<string>();
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;
}
}
}

View file

@ -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<string, HashSet<string>>?)[]? _lastGameObjectResourcePaths;
private (string, Dictionary<string, HashSet<string>>?)[]? _lastPlayerResourcePaths;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastGameObjectResourcesOfType;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _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<ResourceType>());
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<nint, (string, string, ChangedItemIcon)>?)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<T>(string popupId, ref T? result, Action<T> 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<T>((string, T?)[] result, Action<T> drawItem) where T : class
{
var firstSeen = new Dictionary<T, string>();
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<string, HashSet<string>>?)[] 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<nint, (string, string, ChangedItemIcon)>?)[] 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";
}
}

View file

@ -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<string, string> { { _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>(),
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<string, string> { { _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<TemporaryMod> 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);
}
}
}

View file

@ -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<string, float, float> PreSettingsTabBar;
public readonly EventSubscriber<string> PreSettingsPanel;
public readonly EventSubscriber<string> PostEnabled;
public readonly EventSubscriber<string> PostSettingsPanelDraw;
public readonly EventSubscriber<ChangedItemType, uint> ChangedItemTooltip;
public readonly EventSubscriber<MouseButton, ChangedItemType, uint> 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<TabType>())
{
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)}";
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,435 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Api;
using CurrentSettings = ValueTuple<PenumbraApiEc, (bool, int, IDictionary<string, IList<string>>, bool)?>;
public class PenumbraIpcProviders : IDisposable
{
internal readonly IPenumbraApi Api;
// Plugin State
internal readonly EventProvider Initialized;
internal readonly EventProvider Disposed;
internal readonly FuncProvider<int> ApiVersion;
internal readonly FuncProvider<(int Breaking, int Features)> ApiVersions;
internal readonly FuncProvider<bool> GetEnabledState;
internal readonly EventProvider<bool> EnabledChange;
// Configuration
internal readonly FuncProvider<string> GetModDirectory;
internal readonly FuncProvider<string> GetConfiguration;
internal readonly EventProvider<string, bool> ModDirectoryChanged;
// UI
internal readonly EventProvider<string, float, float> PreSettingsTabBarDraw;
internal readonly EventProvider<string> PreSettingsDraw;
internal readonly EventProvider<string> PostEnabledDraw;
internal readonly EventProvider<string> PostSettingsDraw;
internal readonly EventProvider<ChangedItemType, uint> ChangedItemTooltip;
internal readonly EventProvider<MouseButton, ChangedItemType, uint> ChangedItemClick;
internal readonly FuncProvider<TabType, string, string, PenumbraApiEc> OpenMainWindow;
internal readonly ActionProvider CloseMainWindow;
// Redrawing
internal readonly ActionProvider<RedrawType> RedrawAll;
internal readonly ActionProvider<GameObject, RedrawType> RedrawObject;
internal readonly ActionProvider<int, RedrawType> RedrawObjectByIndex;
internal readonly ActionProvider<string, RedrawType> RedrawObjectByName;
internal readonly EventProvider<nint, int> GameObjectRedrawn;
// Game State
internal readonly FuncProvider<nint, (nint, string)> GetDrawObjectInfo;
internal readonly FuncProvider<int, int> GetCutsceneParentIndex;
internal readonly FuncProvider<int, int, PenumbraApiEc> SetCutsceneParentIndex;
internal readonly EventProvider<nint, string, nint, nint, nint> CreatingCharacterBase;
internal readonly EventProvider<nint, string, nint> CreatedCharacterBase;
internal readonly EventProvider<nint, string, string> GameObjectResourcePathResolved;
// Resolve
internal readonly FuncProvider<string, string> ResolveDefaultPath;
internal readonly FuncProvider<string, string> ResolveInterfacePath;
internal readonly FuncProvider<string, string> ResolvePlayerPath;
internal readonly FuncProvider<string, int, string> ResolveGameObjectPath;
internal readonly FuncProvider<string, string, string> ResolveCharacterPath;
internal readonly FuncProvider<string, string, string[]> ReverseResolvePath;
internal readonly FuncProvider<string, int, string[]> ReverseResolveGameObjectPath;
internal readonly FuncProvider<string, string[]> ReverseResolvePlayerPath;
internal readonly FuncProvider<string[], string[], (string[], string[][])> ResolvePlayerPaths;
internal readonly FuncProvider<string[], string[], Task<(string[], string[][])>> ResolvePlayerPathsAsync;
// Collections
internal readonly FuncProvider<IList<string>> GetCollections;
internal readonly FuncProvider<string> GetCurrentCollectionName;
internal readonly FuncProvider<string> GetDefaultCollectionName;
internal readonly FuncProvider<string> GetInterfaceCollectionName;
internal readonly FuncProvider<string, (string, bool)> GetCharacterCollectionName;
internal readonly FuncProvider<ApiCollectionType, string> GetCollectionForType;
internal readonly FuncProvider<ApiCollectionType, string, bool, bool, (PenumbraApiEc, string)> SetCollectionForType;
internal readonly FuncProvider<int, (bool, bool, string)> GetCollectionForObject;
internal readonly FuncProvider<int, string, bool, bool, (PenumbraApiEc, string)> SetCollectionForObject;
internal readonly FuncProvider<string, IReadOnlyDictionary<string, object?>> GetChangedItems;
// Meta
internal readonly FuncProvider<string> GetPlayerMetaManipulations;
internal readonly FuncProvider<string, string> GetMetaManipulations;
internal readonly FuncProvider<int, string> GetGameObjectMetaManipulations;
// Mods
internal readonly FuncProvider<IList<(string, string)>> GetMods;
internal readonly FuncProvider<string, string, PenumbraApiEc> ReloadMod;
internal readonly FuncProvider<string, PenumbraApiEc> InstallMod;
internal readonly FuncProvider<string, PenumbraApiEc> AddMod;
internal readonly FuncProvider<string, string, PenumbraApiEc> DeleteMod;
internal readonly FuncProvider<string, string, (PenumbraApiEc, string, bool)> GetModPath;
internal readonly FuncProvider<string, string, string, PenumbraApiEc> SetModPath;
internal readonly EventProvider<string> ModDeleted;
internal readonly EventProvider<string> ModAdded;
internal readonly EventProvider<string, string> ModMoved;
// ModSettings
internal readonly FuncProvider<string, string, IDictionary<string, (IList<string>, GroupType)>?> GetAvailableModSettings;
internal readonly FuncProvider<string, string, string, bool, CurrentSettings> GetCurrentModSettings;
internal readonly FuncProvider<string, string, string, bool, PenumbraApiEc> TryInheritMod;
internal readonly FuncProvider<string, string, string, bool, PenumbraApiEc> TrySetMod;
internal readonly FuncProvider<string, string, string, int, PenumbraApiEc> TrySetModPriority;
internal readonly FuncProvider<string, string, string, string, string, PenumbraApiEc> TrySetModSetting;
internal readonly FuncProvider<string, string, string, string, IReadOnlyList<string>, PenumbraApiEc> TrySetModSettings;
internal readonly EventProvider<ModSettingChange, string, string, bool> ModSettingChanged;
internal readonly FuncProvider<string, string, string, PenumbraApiEc> CopyModSettings;
// Editing
internal readonly FuncProvider<string, string, TextureType, bool, Task> ConvertTextureFile;
internal readonly FuncProvider<byte[], int, string, TextureType, bool, Task> ConvertTextureData;
// Temporary
internal readonly FuncProvider<string, string, bool, (PenumbraApiEc, string)> CreateTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> RemoveTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> CreateNamedTemporaryCollection;
internal readonly FuncProvider<string, PenumbraApiEc> RemoveTemporaryCollectionByName;
internal readonly FuncProvider<string, int, bool, PenumbraApiEc> AssignTemporaryCollection;
internal readonly FuncProvider<string, Dictionary<string, string>, string, int, PenumbraApiEc> AddTemporaryModAll;
internal readonly FuncProvider<string, string, Dictionary<string, string>, string, int, PenumbraApiEc> AddTemporaryMod;
internal readonly FuncProvider<string, int, PenumbraApiEc> RemoveTemporaryModAll;
internal readonly FuncProvider<string, string, int, PenumbraApiEc> RemoveTemporaryMod;
// Resource Tree
internal readonly FuncProvider<ushort[], IReadOnlyDictionary<string, string[]>?[]> GetGameObjectResourcePaths;
internal readonly FuncProvider<IReadOnlyDictionary<ushort, IReadOnlyDictionary<string, string[]>>> GetPlayerResourcePaths;
internal readonly FuncProvider<ResourceType, bool, ushort[], IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?[]>
GetGameObjectResourcesOfType;
internal readonly
FuncProvider<ResourceType, bool, IReadOnlyDictionary<ushort, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>>>
GetPlayerResourcesOfType;
internal readonly FuncProvider<bool, ushort[], Ipc.ResourceTree?[]> GetGameObjectResourceTrees;
internal readonly FuncProvider<bool, IReadOnlyDictionary<ushort, Ipc.ResourceTree>> GetPlayerResourceTrees;
public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections,
TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config)
{
Api = api;
// Plugin State
Initialized = Ipc.Initialized.Provider(pi);
Disposed = Ipc.Disposed.Provider(pi);
ApiVersion = Ipc.ApiVersion.Provider(pi, DeprecatedVersion);
ApiVersions = Ipc.ApiVersions.Provider(pi, () => Api.ApiVersion);
GetEnabledState = Ipc.GetEnabledState.Provider(pi, Api.GetEnabledState);
EnabledChange =
Ipc.EnabledChange.Provider(pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent);
// Configuration
GetModDirectory = Ipc.GetModDirectory.Provider(pi, Api.GetModDirectory);
GetConfiguration = Ipc.GetConfiguration.Provider(pi, Api.GetConfiguration);
ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a);
// UI
PreSettingsTabBarDraw =
Ipc.PreSettingsTabBarDraw.Provider(pi, a => Api.PreSettingsTabBarDraw += a, a => Api.PreSettingsTabBarDraw -= a);
PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a);
PostEnabledDraw =
Ipc.PostEnabledDraw.Provider(pi, a => Api.PostEnabledDraw += a, a => Api.PostEnabledDraw -= a);
PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a);
ChangedItemTooltip =
Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip);
ChangedItemClick = Ipc.ChangedItemClick.Provider(pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick);
OpenMainWindow = Ipc.OpenMainWindow.Provider(pi, Api.OpenMainWindow);
CloseMainWindow = Ipc.CloseMainWindow.Provider(pi, Api.CloseMainWindow);
// Redrawing
RedrawAll = Ipc.RedrawAll.Provider(pi, Api.RedrawAll);
RedrawObject = Ipc.RedrawObject.Provider(pi, Api.RedrawObject);
RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider(pi, Api.RedrawObject);
RedrawObjectByName = Ipc.RedrawObjectByName.Provider(pi, Api.RedrawObject);
GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider(pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn,
() => Api.GameObjectRedrawn -= OnGameObjectRedrawn);
// Game State
GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider(pi, Api.GetDrawObjectInfo);
GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider(pi, Api.GetCutsceneParentIndex);
SetCutsceneParentIndex = Ipc.SetCutsceneParentIndex.Provider(pi, Api.SetCutsceneParentIndex);
CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider(pi,
() => Api.CreatingCharacterBase += CreatingCharacterBaseEvent,
() => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent);
CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider(pi,
() => Api.CreatedCharacterBase += CreatedCharacterBaseEvent,
() => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent);
GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider(pi,
() => Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent,
() => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent);
// Resolve
ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider(pi, Api.ResolveDefaultPath);
ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider(pi, Api.ResolveInterfacePath);
ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider(pi, Api.ResolvePlayerPath);
ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider(pi, Api.ResolveGameObjectPath);
ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider(pi, Api.ResolvePath);
ReverseResolvePath = Ipc.ReverseResolvePath.Provider(pi, Api.ReverseResolvePath);
ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider(pi, Api.ReverseResolveGameObjectPath);
ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider(pi, Api.ReverseResolvePlayerPath);
ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider(pi, Api.ResolvePlayerPaths);
ResolvePlayerPathsAsync = Ipc.ResolvePlayerPathsAsync.Provider(pi, Api.ResolvePlayerPathsAsync);
// Collections
GetCollections = Ipc.GetCollections.Provider(pi, Api.GetCollections);
GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider(pi, Api.GetCurrentCollection);
GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider(pi, Api.GetDefaultCollection);
GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider(pi, Api.GetInterfaceCollection);
GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider(pi, Api.GetCharacterCollection);
GetCollectionForType = Ipc.GetCollectionForType.Provider(pi, Api.GetCollectionForType);
SetCollectionForType = Ipc.SetCollectionForType.Provider(pi, Api.SetCollectionForType);
GetCollectionForObject = Ipc.GetCollectionForObject.Provider(pi, Api.GetCollectionForObject);
SetCollectionForObject = Ipc.SetCollectionForObject.Provider(pi, Api.SetCollectionForObject);
GetChangedItems = Ipc.GetChangedItems.Provider(pi, Api.GetChangedItemsForCollection);
// Meta
GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider(pi, Api.GetPlayerMetaManipulations);
GetMetaManipulations = Ipc.GetMetaManipulations.Provider(pi, Api.GetMetaManipulations);
GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider(pi, Api.GetGameObjectMetaManipulations);
// Mods
GetMods = Ipc.GetMods.Provider(pi, Api.GetModList);
ReloadMod = Ipc.ReloadMod.Provider(pi, Api.ReloadMod);
InstallMod = Ipc.InstallMod.Provider(pi, Api.InstallMod);
AddMod = Ipc.AddMod.Provider(pi, Api.AddMod);
DeleteMod = Ipc.DeleteMod.Provider(pi, Api.DeleteMod);
GetModPath = Ipc.GetModPath.Provider(pi, Api.GetModPath);
SetModPath = Ipc.SetModPath.Provider(pi, Api.SetModPath);
ModDeleted = Ipc.ModDeleted.Provider(pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent);
ModAdded = Ipc.ModAdded.Provider(pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent);
ModMoved = Ipc.ModMoved.Provider(pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent);
// ModSettings
GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider(pi, Api.GetAvailableModSettings);
GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider(pi, Api.GetCurrentModSettings);
TryInheritMod = Ipc.TryInheritMod.Provider(pi, Api.TryInheritMod);
TrySetMod = Ipc.TrySetMod.Provider(pi, Api.TrySetMod);
TrySetModPriority = Ipc.TrySetModPriority.Provider(pi, Api.TrySetModPriority);
TrySetModSetting = Ipc.TrySetModSetting.Provider(pi, Api.TrySetModSetting);
TrySetModSettings = Ipc.TrySetModSettings.Provider(pi, Api.TrySetModSettings);
ModSettingChanged = Ipc.ModSettingChanged.Provider(pi,
() => Api.ModSettingChanged += ModSettingChangedEvent,
() => Api.ModSettingChanged -= ModSettingChangedEvent);
CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings);
// Editing
ConvertTextureFile = Ipc.ConvertTextureFile.Provider(pi, Api.ConvertTextureFile);
ConvertTextureData = Ipc.ConvertTextureData.Provider(pi, Api.ConvertTextureData);
// Temporary
CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection);
RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection);
CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider(pi, Api.CreateNamedTemporaryCollection);
RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider(pi, Api.RemoveTemporaryCollectionByName);
AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider(pi, Api.AssignTemporaryCollection);
AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider(pi, Api.AddTemporaryModAll);
AddTemporaryMod = Ipc.AddTemporaryMod.Provider(pi, Api.AddTemporaryMod);
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll);
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod);
// ResourceTree
GetGameObjectResourcePaths = Ipc.GetGameObjectResourcePaths.Provider(pi, Api.GetGameObjectResourcePaths);
GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths);
GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType);
GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType);
GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees);
GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees);
Initialized.Invoke();
}
public void Dispose()
{
// Plugin State
Initialized.Dispose();
ApiVersion.Dispose();
ApiVersions.Dispose();
GetEnabledState.Dispose();
EnabledChange.Dispose();
// Configuration
GetModDirectory.Dispose();
GetConfiguration.Dispose();
ModDirectoryChanged.Dispose();
// UI
PreSettingsTabBarDraw.Dispose();
PreSettingsDraw.Dispose();
PostEnabledDraw.Dispose();
PostSettingsDraw.Dispose();
ChangedItemTooltip.Dispose();
ChangedItemClick.Dispose();
OpenMainWindow.Dispose();
CloseMainWindow.Dispose();
// Redrawing
RedrawAll.Dispose();
RedrawObject.Dispose();
RedrawObjectByIndex.Dispose();
RedrawObjectByName.Dispose();
GameObjectRedrawn.Dispose();
// Game State
GetDrawObjectInfo.Dispose();
GetCutsceneParentIndex.Dispose();
SetCutsceneParentIndex.Dispose();
CreatingCharacterBase.Dispose();
CreatedCharacterBase.Dispose();
GameObjectResourcePathResolved.Dispose();
// Resolve
ResolveDefaultPath.Dispose();
ResolveInterfacePath.Dispose();
ResolvePlayerPath.Dispose();
ResolveGameObjectPath.Dispose();
ResolveCharacterPath.Dispose();
ReverseResolvePath.Dispose();
ReverseResolveGameObjectPath.Dispose();
ReverseResolvePlayerPath.Dispose();
ResolvePlayerPaths.Dispose();
ResolvePlayerPathsAsync.Dispose();
// Collections
GetCollections.Dispose();
GetCurrentCollectionName.Dispose();
GetDefaultCollectionName.Dispose();
GetInterfaceCollectionName.Dispose();
GetCharacterCollectionName.Dispose();
GetCollectionForType.Dispose();
SetCollectionForType.Dispose();
GetCollectionForObject.Dispose();
SetCollectionForObject.Dispose();
GetChangedItems.Dispose();
// Meta
GetPlayerMetaManipulations.Dispose();
GetMetaManipulations.Dispose();
GetGameObjectMetaManipulations.Dispose();
// Mods
GetMods.Dispose();
ReloadMod.Dispose();
InstallMod.Dispose();
AddMod.Dispose();
DeleteMod.Dispose();
GetModPath.Dispose();
SetModPath.Dispose();
ModDeleted.Dispose();
ModAdded.Dispose();
ModMoved.Dispose();
// ModSettings
GetAvailableModSettings.Dispose();
GetCurrentModSettings.Dispose();
TryInheritMod.Dispose();
TrySetMod.Dispose();
TrySetModPriority.Dispose();
TrySetModSetting.Dispose();
TrySetModSettings.Dispose();
ModSettingChanged.Dispose();
CopyModSettings.Dispose();
// Temporary
CreateTemporaryCollection.Dispose();
RemoveTemporaryCollection.Dispose();
CreateNamedTemporaryCollection.Dispose();
RemoveTemporaryCollectionByName.Dispose();
AssignTemporaryCollection.Dispose();
AddTemporaryModAll.Dispose();
AddTemporaryMod.Dispose();
RemoveTemporaryModAll.Dispose();
RemoveTemporaryMod.Dispose();
// Editing
ConvertTextureFile.Dispose();
ConvertTextureData.Dispose();
// Resource Tree
GetGameObjectResourcePaths.Dispose();
GetPlayerResourcePaths.Dispose();
GetGameObjectResourcesOfType.Dispose();
GetPlayerResourcesOfType.Dispose();
GetGameObjectResourceTrees.Dispose();
GetPlayerResourceTrees.Dispose();
Disposed.Invoke();
Disposed.Dispose();
}
// Wrappers
private int DeprecatedVersion()
{
Penumbra.Log.Warning($"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead.");
return Api.ApiVersion.Breaking;
}
private void OnClick(MouseButton click, object? item)
{
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item);
ChangedItemClick.Invoke(click, type, id);
}
private void OnTooltip(object? item)
{
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item);
ChangedItemTooltip.Invoke(type, id);
}
private void EnabledChangeEvent(bool value)
=> EnabledChange.Invoke(value);
private void OnGameObjectRedrawn(IntPtr objectAddress, int objectTableIndex)
=> GameObjectRedrawn.Invoke(objectAddress, objectTableIndex);
private void CreatingCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData)
=> CreatingCharacterBase.Invoke(gameObject, collectionName, modelId, customize, equipData);
private void CreatedCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr drawObject)
=> CreatedCharacterBase.Invoke(gameObject, collectionName, drawObject);
private void GameObjectResourceResolvedEvent(IntPtr gameObject, string gamePath, string localPath)
=> GameObjectResourcePathResolved.Invoke(gameObject, gamePath, localPath);
private void ModSettingChangedEvent(ModSettingChange type, string collection, string mod, bool inherited)
=> ModSettingChanged.Invoke(type, collection, mod, inherited);
private void ModDeletedEvent(string name)
=> ModDeleted.Invoke(name);
private void ModAddedEvent(string name)
=> ModAdded.Invoke(name);
private void ModMovedEvent(string from, string to)
=> ModMoved.Invoke(from, to);
}

View file

@ -243,14 +243,14 @@ public sealed class CollectionCache : IDisposable
continue;
var config = settings.Settings[groupIndex];
switch (group.Type)
switch (group)
{
case GroupType.Single:
AddSubMod(group[config.AsIndex], mod);
case SingleModGroup single:
AddSubMod(single[config.AsIndex], mod);
break;
case GroupType.Multi:
case MultiModGroup multi:
{
foreach (var (option, _) in group.WithIndex()
foreach (var (option, _) in multi.WithIndex()
.Where(p => config.HasFlag(p.Index))
.OrderByDescending(p => group.OptionPriority(p.Index)))
AddSubMod(option, mod);

View file

@ -119,7 +119,7 @@ public class CollectionCacheManager : IDisposable
/// Does not create caches.
/// </summary>
public void CalculateEffectiveFileList(ModCollection collection)
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name,
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier,
() => CalculateEffectiveFileListInternal(collection));
private void CalculateEffectiveFileListInternal(ModCollection collection)

View file

@ -116,7 +116,7 @@ public readonly struct ImcCache : IDisposable
}
private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path)
=> new($"|{collection.Name}_{collection.ChangeCounter}|{path}");
=> new($"|{collection.Id.OptimizedString()}_{collection.ChangeCounter}|{path}");
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
=> _imcFiles.TryGetValue(path, out file);

View file

@ -22,7 +22,7 @@ public class ActiveCollectionData
public class ActiveCollections : ISavable, IDisposable
{
public const int Version = 1;
public const int Version = 2;
private readonly CollectionStorage _storage;
private readonly CommunicatorService _communicator;
@ -261,16 +261,17 @@ public class ActiveCollections : ISavable, IDisposable
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Name },
{ nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name },
{ nameof(Default), Default.Id },
{ nameof(Interface), Interface.Id },
{ nameof(Current), Current.Id },
};
foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Name);
jObj.Add(type.ToString(), collection.Id);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
jObj.WriteTo(j);
}
@ -319,22 +320,16 @@ public class ActiveCollections : ISavable, IDisposable
}
}
/// <summary>
/// Load default, current, special, and character collections from config.
/// If a collection does not exist anymore, reset it to an appropriate default.
/// </summary>
private void LoadCollections()
private bool LoadCollectionsV1(JObject jObject)
{
Penumbra.Log.Debug("[Collections] Reading collection assignments...");
var configChanged = !Load(_saveService.FileNames, out var jObject);
// Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed.
var defaultName = jObject[nameof(Default)]?.ToObject<string>()
?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name);
var configChanged = false;
// Load the default collection. If the name does not exist take the empty collection.
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? ModCollection.Empty.Name;
if (!_storage.ByName(defaultName, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning);
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
}
@ -348,7 +343,8 @@ public class ActiveCollections : ISavable, IDisposable
if (!_storage.ByName(interfaceName, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning);
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
@ -362,7 +358,8 @@ public class ActiveCollections : ISavable, IDisposable
if (!_storage.ByName(currentName, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", NotificationType.Warning);
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
}
@ -393,11 +390,124 @@ public class ActiveCollections : ISavable, IDisposable
Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments.");
configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 1);
// Save any changes.
if (configChanged)
_saveService.ImmediateSave(this);
return configChanged;
}
private bool LoadCollectionsV2(JObject jObject)
{
var configChanged = false;
// Load the default collection. If the guid does not exist take the empty collection.
var defaultId = jObject[nameof(Default)]?.ToObject<Guid>() ?? Guid.Empty;
if (!_storage.ById(defaultId, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
}
else
{
Default = defaultCollection;
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
var interfaceId = jObject[nameof(Interface)]?.ToObject<Guid>() ?? Default.Id;
if (!_storage.ById(interfaceId, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
else
{
Interface = interfaceCollection;
}
// Load the current collection.
var currentId = jObject[nameof(Current)]?.ToObject<Guid>() ?? _storage.DefaultNamed.Id;
if (!_storage.ById(currentId, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
}
else
{
Current = currentCollection;
}
// Load special collections.
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{
var typeId = jObject[type.ToString()]?.ToObject<Guid>();
if (typeId == null)
continue;
if (!_storage.ById(typeId.Value, out var typeCollection))
{
Penumbra.Messager.NotificationMessage($"Last choice of {name} Collection {typeId.Value} is not available, removed.",
NotificationType.Warning);
configChanged = true;
}
else
{
SpecialCollections[(int)type] = typeCollection;
}
}
Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments.");
configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject);
configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 2);
return configChanged;
}
private bool LoadCollectionsNew()
{
Current = _storage.DefaultNamed;
Default = _storage.DefaultNamed;
Interface = _storage.DefaultNamed;
return true;
}
/// <summary>
/// Load default, current, special, and character collections from config.
/// If a collection does not exist anymore, reset it to an appropriate default.
/// </summary>
private void LoadCollections()
{
Penumbra.Log.Debug("[Collections] Reading collection assignments...");
var configChanged = !Load(_saveService.FileNames, out var jObject);
var version = jObject["Version"]?.ToObject<int>() ?? 0;
var changed = false;
switch (version)
{
case 1:
changed = LoadCollectionsV1(jObject);
break;
case 2:
changed = LoadCollectionsV2(jObject);
break;
case 0 when configChanged:
changed = LoadCollectionsNew();
break;
case 0:
Penumbra.Messager.NotificationMessage("Active Collections File has unknown version and will be reset.",
NotificationType.Warning);
changed = LoadCollectionsNew();
break;
}
if (changed)
_saveService.ImmediateSaveSync(this);
}
/// <summary>
@ -410,7 +520,7 @@ public class ActiveCollections : ISavable, IDisposable
var jObj = BackupService.GetJObjectForFile(fileNames, file);
if (jObj == null)
{
ret = new JObject();
ret = [];
return false;
}

View file

@ -1,7 +1,6 @@
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
@ -48,6 +47,25 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
return true;
}
/// <summary> Find a collection by its id. If the GUID is empty, the empty collection is returned. </summary>
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
{
if (id != Guid.Empty)
return _collections.FindFirst(c => c.Id == id, out collection);
collection = ModCollection.Empty;
return true;
}
/// <summary> Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. </summary>
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
{
if (Guid.TryParse(identifier, out var guid))
return ById(guid, out collection);
return ByName(identifier, out collection);
}
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
@ -70,31 +88,6 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
}
/// <summary>
/// Returns true if the name is not empty, it is not the name of the empty collection
/// and no existing collection results in the same filename as name. Also returns the fixed name.
/// </summary>
public bool CanAddCollection(string name, out string fixedName)
{
if (!IsValidName(name))
{
fixedName = string.Empty;
return false;
}
name = name.ToLowerInvariant();
if (name.Length == 0
|| name == ModCollection.Empty.Name.ToLowerInvariant()
|| _collections.Any(c => c.Name.ToLowerInvariant() == name))
{
fixedName = string.Empty;
return false;
}
fixedName = name;
return true;
}
/// <summary>
/// Add a new collection of the given name.
/// If duplicate is not-null, the new collection will be a duplicate of it.
@ -104,14 +97,6 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
if (!CanAddCollection(name, out var fixedName))
{
Penumbra.Messager.NotificationMessage(
$"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning,
false);
return false;
}
var newCollection = duplicate?.Duplicate(name, _collections.Count)
?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
_collections.Add(newCollection);
@ -166,16 +151,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
/// <summary>
/// Check if a name is valid to use for a collection.
/// Does not check for uniqueness.
/// </summary>
private static bool IsValidName(string name)
=> name.Length is > 0 and < 64 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath());
/// <summary>
/// Read all collection files in the Collection Directory.
/// Ensure that the default named collection exists, and apply inheritances afterwards.
/// Ensure that the default named collection exists, and apply inheritances afterward.
/// Duplicate collection files are not deleted, just not added here.
/// </summary>
private void ReadCollections(out ModCollection defaultNamedCollection)
@ -183,29 +161,46 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
Penumbra.Log.Debug("[Collections] Reading saved collections...");
foreach (var file in _saveService.FileNames.CollectionFiles)
{
if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance))
if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance))
continue;
if (!IsValidName(name))
if (id == Guid.Empty)
{
// TODO: handle better.
Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.",
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
continue;
}
if (ById(id, out _))
{
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
NotificationType.Warning);
continue;
}
if (ByName(name, out _))
{
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.",
NotificationType.Warning);
continue;
}
var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance);
var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.",
NotificationType.Warning);
try
{
if (version >= 2)
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.",
NotificationType.Warning);
}
else
{
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
File.Delete(file.FullName);
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}.");
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", NotificationType.Error);
}
_collections.Add(collection);
}

View file

@ -18,7 +18,7 @@ public partial class IndividualCollections
foreach (var (name, identifiers, collection) in Assignments)
{
var tmp = identifiers[0].ToJson();
tmp.Add("Collection", collection.Name);
tmp.Add("Collection", collection.Id);
tmp.Add("Display", name);
ret.Add(tmp);
}
@ -26,18 +26,28 @@ public partial class IndividualCollections
return ret;
}
public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage)
public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage, int version)
{
if (_actors.Awaiter.IsCompletedSuccessfully)
{
var ret = ReadJObjectInternal(obj, storage);
var ret = version switch
{
1 => ReadJObjectInternalV1(obj, storage),
2 => ReadJObjectInternalV2(obj, storage),
_ => true,
};
return ret;
}
Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready...");
_actors.Awaiter.ContinueWith(_ =>
{
if (ReadJObjectInternal(obj, storage))
if (version switch
{
1 => ReadJObjectInternalV1(obj, storage),
2 => ReadJObjectInternalV2(obj, storage),
_ => true,
})
saver.ImmediateSave(parent);
IsLoaded = true;
Loaded.Invoke();
@ -45,7 +55,55 @@ public partial class IndividualCollections
return false;
}
private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage)
private bool ReadJObjectInternalV1(JArray? obj, CollectionStorage storage)
{
Penumbra.Log.Debug("[Collections] Reading individual assignments...");
if (obj == null)
{
Penumbra.Log.Debug($"[Collections] Finished reading {Count} individual assignments...");
return true;
}
foreach (var data in obj)
{
try
{
var identifier = _actors.FromJson(data as JObject);
var group = GetGroup(identifier);
if (group.Length == 0 || group.Any(i => !i.IsValid))
{
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.",
NotificationType.Error);
continue;
}
var collectionName = data["Collection"]?.ToObject<string>() ?? string.Empty;
if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection))
{
Penumbra.Messager.NotificationMessage(
$"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.",
NotificationType.Warning);
continue;
}
if (!Add(group, collection))
{
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.",
NotificationType.Warning);
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error);
}
}
Penumbra.Log.Debug($"Finished reading {Count} individual assignments...");
return true;
}
private bool ReadJObjectInternalV2(JArray? obj, CollectionStorage storage)
{
Penumbra.Log.Debug("[Collections] Reading individual assignments...");
if (obj == null)
@ -64,17 +122,17 @@ public partial class IndividualCollections
if (group.Length == 0 || group.Any(i => !i.IsValid))
{
changes = true;
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.",
Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed assignment.",
NotificationType.Error);
continue;
}
var collectionName = data["Collection"]?.ToObject<string>() ?? string.Empty;
if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection))
var collectionId = data["Collection"]?.ToObject<Guid>();
if (!collectionId.HasValue || !storage.ById(collectionId.Value, out var collection))
{
changes = true;
Penumbra.Messager.NotificationMessage(
$"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.",
$"Could not load the collection {collectionId} as individual collection for {identifier}, removed assignment.",
NotificationType.Warning);
continue;
}
@ -82,14 +140,14 @@ public partial class IndividualCollections
if (!Add(group, collection))
{
changes = true;
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.",
Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed assignment.",
NotificationType.Warning);
}
}
catch (Exception e)
{
changes = true;
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error);
Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed assignment.", NotificationType.Error);
}
}
@ -100,14 +158,6 @@ public partial class IndividualCollections
internal void Migrate0To1(Dictionary<string, ModCollection> old)
{
static bool FindDataId(string name, NameDictionary data, out NpcId dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<NpcId, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
foreach (var (name, collection) in old)
{
var kind = ObjectKind.None;
@ -155,5 +205,15 @@ public partial class IndividualCollections
NotificationType.Error);
}
}
return;
static bool FindDataId(string name, NameDictionary data, out NpcId dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<NpcId, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
}
}

View file

@ -138,7 +138,7 @@ public class InheritanceManager : IDisposable
var changes = false;
foreach (var subCollectionName in collection.InheritanceByName)
{
if (_storage.ByName(subCollectionName, out var subCollection))
if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection))
{
if (AddInheritance(collection, subCollection, false))
continue;
@ -146,6 +146,15 @@ public class InheritanceManager : IDisposable
changes = true;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
}
else if (_storage.ByName(subCollectionName, out subCollection))
{
changes = true;
Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID.");
if (AddInheritance(collection, subCollection, false))
continue;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
}
else
{
Penumbra.Messager.NotificationMessage(

View file

@ -1,3 +1,4 @@
using OtterGui;
using Penumbra.Api;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
@ -9,13 +10,13 @@ namespace Penumbra.Collections.Manager;
public class TempCollectionManager : IDisposable
{
public int GlobalChangeCounter { get; private set; } = 0;
public int GlobalChangeCounter { get; private set; }
public readonly IndividualCollections Collections;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActorManager _actors;
private readonly Dictionary<string, ModCollection> _customCollections = new();
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActorManager _actors;
private readonly Dictionary<Guid, ModCollection> _customCollections = [];
public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorManager actors, CollectionStorage storage)
{
@ -42,36 +43,36 @@ public class TempCollectionManager : IDisposable
=> _customCollections.Values;
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(name.ToLowerInvariant(), out collection);
=> _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection);
public string CreateTemporaryCollection(string name)
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(id, out collection);
public Guid CreateTemporaryCollection(string name)
{
if (_storage.ByName(name, out _))
return string.Empty;
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++);
Penumbra.Log.Debug($"Creating temporary collection {collection.AnonymizedName}.");
if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection))
Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}.");
if (_customCollections.TryAdd(collection.Id, collection))
{
// Temporary collection created.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty);
return collection.Name;
return collection.Id;
}
return string.Empty;
return Guid.Empty;
}
public bool RemoveTemporaryCollection(string collectionName)
public bool RemoveTemporaryCollection(Guid collectionId)
{
if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection))
if (!_customCollections.Remove(collectionId, out var collection))
{
Penumbra.Log.Debug($"Tried to delete temporary collection {collectionName.ToLowerInvariant()}, but did not exist.");
Penumbra.Log.Debug($"Tried to delete temporary collection {collectionId}, but did not exist.");
return false;
}
Penumbra.Log.Debug($"Deleted temporary collection {collection.AnonymizedName}.");
Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}.");
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
for (var i = 0; i < Collections.Count; ++i)
{
@ -80,7 +81,7 @@ public class TempCollectionManager : IDisposable
// Temporary collection assignment removed.
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.AnonymizedName} from {Collections[i].DisplayName}.");
Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}.");
Collections.Delete(i--);
}
@ -98,32 +99,32 @@ public class TempCollectionManager : IDisposable
return true;
}
public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers)
public bool AddIdentifier(Guid collectionId, params ActorIdentifier[] identifiers)
{
if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection))
if (!_customCollections.TryGetValue(collectionId, out var collection))
return false;
return AddIdentifier(collection, identifiers);
}
public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue)
public bool AddIdentifier(Guid collectionId, string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
if (!ByteString.FromString(characterName, out var byteString))
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
if (!identifier.IsValid)
return false;
return AddIdentifier(collectionName, identifier);
return AddIdentifier(collectionId, identifier);
}
internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
if (!ByteString.FromString(characterName, out var byteString))
return false;
var identifier = _actors.CreatePlayer(byteString, worldId);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name);
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id);
}
}

View file

@ -17,7 +17,7 @@ namespace Penumbra.Collections;
/// </summary>
public partial class ModCollection
{
public const int CurrentVersion = 1;
public const int CurrentVersion = 2;
public const string DefaultCollectionName = "Default";
public const string EmptyCollectionName = "None";
@ -27,15 +27,23 @@ public partial class ModCollection
/// </summary>
public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0);
/// <summary> The name of a collection can not contain characters invalid in a path. </summary>
public string Name { get; internal init; }
/// <summary> The name of a collection. </summary>
public string Name { get; set; } = string.Empty;
public Guid Id { get; }
public string Identifier
=> Id.ToString();
public string ShortIdentifier
=> Identifier[..8];
public override string ToString()
=> Name;
=> Name.Length > 0 ? Name : ShortIdentifier;
/// <summary> Get the first two letters of a collection name and its Index (or None if it is the empty collection). </summary>
public string AnonymizedName
=> this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
=> this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier;
/// <summary> The index of the collection is set and kept up-to-date by the CollectionManager. </summary>
public int Index { get; internal set; }
@ -112,16 +120,16 @@ public partial class ModCollection
public ModCollection Duplicate(string name, int index)
{
Debug.Assert(index > 0, "Collection duplicated with non-positive index.");
return new ModCollection(name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
[.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()));
}
/// <summary> Constructor for reading from files. </summary>
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, string name, int version, int index,
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index,
Dictionary<string, ModSettings.SavedSettings> allSettings, IReadOnlyList<string> inheritances)
{
Debug.Assert(index > 0, "Collection read with non-positive index.");
var ret = new ModCollection(name, index, 0, version, new List<ModSettings?>(), new List<ModCollection>(), allSettings)
var ret = new ModCollection(id, name, index, 0, version, [], [], allSettings)
{
InheritanceByName = inheritances,
};
@ -134,8 +142,7 @@ public partial class ModCollection
public static ModCollection CreateTemporary(string name, int index, int changeCounter)
{
Debug.Assert(index < 0, "Temporary collection created with non-negative index.");
var ret = new ModCollection(name, index, changeCounter, CurrentVersion, new List<ModSettings?>(), new List<ModCollection>(),
new Dictionary<string, ModSettings.SavedSettings>());
var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []);
return ret;
}
@ -143,9 +150,8 @@ public partial class ModCollection
public static ModCollection CreateEmpty(string name, int index, int modCount)
{
Debug.Assert(index >= 0, "Empty collection created with negative index.");
return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(),
new List<ModCollection>(),
new Dictionary<string, ModSettings.SavedSettings>());
return new ModCollection(Guid.Empty, name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [],
[]);
}
/// <summary> Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. </summary>
@ -193,10 +199,11 @@ public partial class ModCollection
saver.ImmediateSave(new ModCollectionSave(mods, this));
}
private ModCollection(string name, int index, int changeCounter, int version, List<ModSettings?> appliedSettings,
private ModCollection(Guid id, string name, int index, int changeCounter, int version, List<ModSettings?> appliedSettings,
List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
{
Name = name;
Id = id;
Index = index;
ChangeCounter = changeCounter;
Settings = appliedSettings;

View file

@ -28,6 +28,8 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
j.WriteStartObject();
j.WritePropertyName("Version");
j.WriteValue(ModCollection.CurrentVersion);
j.WritePropertyName(nameof(ModCollection.Id));
j.WriteValue(modCollection.Identifier);
j.WritePropertyName(nameof(ModCollection.Name));
j.WriteValue(modCollection.Name);
j.WritePropertyName(nameof(ModCollection.Settings));
@ -55,20 +57,20 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
// Inherit by collection name.
j.WritePropertyName("Inheritance");
x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Name));
x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier));
j.WriteEndObject();
}
public static bool LoadFromFile(FileInfo file, out string name, out int version, out Dictionary<string, ModSettings.SavedSettings> settings,
public static bool LoadFromFile(FileInfo file, out Guid id, out string name, out int version, out Dictionary<string, ModSettings.SavedSettings> settings,
out IReadOnlyList<string> inheritance)
{
settings = new Dictionary<string, ModSettings.SavedSettings>();
inheritance = Array.Empty<string>();
settings = [];
inheritance = [];
if (!file.Exists)
{
Penumbra.Log.Error("Could not read collection because file does not exist.");
name = string.Empty;
name = string.Empty;
id = Guid.Empty;
version = 0;
return false;
}
@ -76,8 +78,9 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
try
{
var obj = JObject.Parse(File.ReadAllText(file.FullName));
name = obj[nameof(ModCollection.Name)]?.ToObject<string>() ?? string.Empty;
version = obj["Version"]?.ToObject<int>() ?? 0;
name = obj[nameof(ModCollection.Name)]?.ToObject<string>() ?? string.Empty;
id = obj[nameof(ModCollection.Id)]?.ToObject<Guid>() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty);
// Custom deserialization that is converted with the constructor.
settings = obj[nameof(ModCollection.Settings)]?.ToObject<Dictionary<string, ModSettings.SavedSettings>>() ?? settings;
inheritance = obj["Inheritance"]?.ToObject<List<string>>() ?? inheritance;
@ -87,6 +90,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection
{
name = string.Empty;
version = 0;
id = Guid.Empty;
Penumbra.Log.Error($"Could not read collection information from file:\n{e}");
return false;
}

View file

@ -513,7 +513,7 @@ public class CommandHandler : IDisposable
collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase)
? ModCollection.Empty
: _collectionManager.Storage.ByName(lowerName, out var c)
: _collectionManager.Storage.ByIdentifier(lowerName, out var c)
? c
: null;
if (collection != null)

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
namespace Penumbra.Communication;
@ -14,7 +15,7 @@ public sealed class ChangedItemClick() : EventWrapper<MouseButton, object?, Chan
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.ChangedItemClicked"/>
/// <seealso cref="UiApi.OnChangedItemClick"/>
Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -12,7 +13,7 @@ public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.ChangedItemTooltip"/>
/// <seealso cref="UiApi.OnChangedItemHover"/>
Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Collections;
namespace Penumbra.Communication;

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Services;
namespace Penumbra.Communication;
@ -14,7 +15,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is a pointer to the equip data array. </item>
/// </list> </summary>
public sealed class CreatingCharacterBase()
: EventWrapper<nint, string, nint, nint, nint, CreatingCharacterBase.Priority>(nameof(CreatingCharacterBase))
: EventWrapper<nint, Guid, nint, nint, nint, CreatingCharacterBase.Priority>(nameof(CreatingCharacterBase))
{
public enum Priority
{

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.IpcSubscribers;
namespace Penumbra.Communication;
@ -13,7 +14,7 @@ public sealed class EnabledChanged() : EventWrapper<bool, EnabledChanged.Priorit
{
public enum Priority
{
/// <seealso cref="Ipc.EnabledChange"/>
/// <seealso cref="Api.IpcSubscribers.Ipc.EnabledChange"/>
Api = int.MinValue,
/// <seealso cref="Api.DalamudSubstitutionProvider.OnEnabledChange"/>

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
namespace Penumbra.Communication;

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Editor;

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Manager;

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
@ -19,8 +20,11 @@ public sealed class ModPathChanged()
{
public enum Priority
{
/// <seealso cref="PenumbraApi.ModPathChangeSubscriber"/>
Api = int.MinValue,
/// <seealso cref="ModsApi.OnModPathChange"/>
ApiMods = int.MinValue,
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
ApiModSettings = int.MinValue,
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
EphemeralConfig = -500,

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Mods;

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -12,7 +13,7 @@ public sealed class PostEnabledDraw() : EventWrapper<string, PostEnabledDraw.Pri
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PostEnabledDraw"/>
/// <seealso cref="PenumbraApi.PostEnabledDraw"/>
Default = 0,
}
}

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -12,7 +13,7 @@ public sealed class PostSettingsPanelDraw() : EventWrapper<string, PostSettingsP
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PostSettingsPanelDraw"/>
/// <seealso cref="PenumbraApi.PostSettingsPanelDraw"/>
Default = 0,
}
}

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -12,7 +13,7 @@ public sealed class PreSettingsPanelDraw() : EventWrapper<string, PreSettingsPan
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PreSettingsPanelDraw"/>
/// <seealso cref="PenumbraApi.PreSettingsPanelDraw"/>
Default = 0,
}
}

View file

@ -1,4 +1,5 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
namespace Penumbra.Communication;
@ -14,7 +15,7 @@ public sealed class PreSettingsTabBarDraw() : EventWrapper<string, float, float,
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PreSettingsTabBarDraw"/>
/// <seealso cref="PenumbraApi.PreSettingsTabBarDraw"/>
Default = 0,
}
}

254
Penumbra/GuidExtensions.cs Normal file
View file

@ -0,0 +1,254 @@
using System.Collections.Frozen;
using OtterGui;
namespace Penumbra;
public static class GuidExtensions
{
private const string Chars =
"0123456789"
+ "abcdefghij"
+ "klmnopqrst"
+ "uv";
private static ReadOnlySpan<byte> Bytes
=> "0123456789abcdefghijklmnopqrstuv"u8;
private static readonly FrozenDictionary<char, byte>
ReverseChars = Chars.WithIndex().ToFrozenDictionary(t => t.Value, t => (byte)t.Index);
private static readonly FrozenDictionary<byte, byte> ReverseBytes =
ReverseChars.ToFrozenDictionary(kvp => (byte)kvp.Key, kvp => kvp.Value);
public static unsafe string OptimizedString(this Guid guid)
{
var bytes = stackalloc ulong[2];
if (!guid.TryWriteBytes(new Span<byte>(bytes, 16)))
return guid.ToString("N");
var u1 = bytes[0];
var u2 = bytes[1];
Span<char> text =
[
Chars[(int)(u1 & 0x1F)],
Chars[(int)((u1 >> 5) & 0x1F)],
Chars[(int)((u1 >> 10) & 0x1F)],
Chars[(int)((u1 >> 15) & 0x1F)],
Chars[(int)((u1 >> 20) & 0x1F)],
Chars[(int)((u1 >> 25) & 0x1F)],
Chars[(int)((u1 >> 30) & 0x1F)],
Chars[(int)((u1 >> 35) & 0x1F)],
Chars[(int)((u1 >> 40) & 0x1F)],
Chars[(int)((u1 >> 45) & 0x1F)],
Chars[(int)((u1 >> 50) & 0x1F)],
Chars[(int)((u1 >> 55) & 0x1F)],
Chars[(int)((u1 >> 60) | ((u2 & 0x01) << 4))],
Chars[(int)((u2 >> 1) & 0x1F)],
Chars[(int)((u2 >> 6) & 0x1F)],
Chars[(int)((u2 >> 11) & 0x1F)],
Chars[(int)((u2 >> 16) & 0x1F)],
Chars[(int)((u2 >> 21) & 0x1F)],
Chars[(int)((u2 >> 26) & 0x1F)],
Chars[(int)((u2 >> 31) & 0x1F)],
Chars[(int)((u2 >> 36) & 0x1F)],
Chars[(int)((u2 >> 41) & 0x1F)],
Chars[(int)((u2 >> 46) & 0x1F)],
Chars[(int)((u2 >> 51) & 0x1F)],
Chars[(int)((u2 >> 56) & 0x1F)],
Chars[(int)((u2 >> 61) & 0x1F)],
];
return new string(text);
}
public static unsafe bool FromOptimizedString(ReadOnlySpan<char> text, out Guid guid)
{
if (text.Length != 26)
return Return(out guid);
var bytes = stackalloc ulong[2];
if (!ReverseChars.TryGetValue(text[0], out var b0))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[1], out var b1))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[2], out var b2))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[3], out var b3))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[4], out var b4))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[5], out var b5))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[6], out var b6))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[7], out var b7))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[8], out var b8))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[9], out var b9))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[10], out var b10))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[11], out var b11))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[12], out var b12))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[13], out var b13))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[14], out var b14))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[15], out var b15))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[16], out var b16))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[17], out var b17))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[18], out var b18))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[19], out var b19))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[20], out var b20))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[21], out var b21))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[22], out var b22))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[23], out var b23))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[24], out var b24))
return Return(out guid);
if (!ReverseChars.TryGetValue(text[25], out var b25))
return Return(out guid);
bytes[0] = b0
| ((ulong)b1 << 5)
| ((ulong)b2 << 10)
| ((ulong)b3 << 15)
| ((ulong)b4 << 20)
| ((ulong)b5 << 25)
| ((ulong)b6 << 30)
| ((ulong)b7 << 35)
| ((ulong)b8 << 40)
| ((ulong)b9 << 45)
| ((ulong)b10 << 50)
| ((ulong)b11 << 55)
| ((ulong)b12 << 60);
bytes[1] = ((ulong)b12 >> 4)
| ((ulong)b13 << 1)
| ((ulong)b14 << 6)
| ((ulong)b15 << 11)
| ((ulong)b16 << 16)
| ((ulong)b17 << 21)
| ((ulong)b18 << 26)
| ((ulong)b19 << 31)
| ((ulong)b20 << 36)
| ((ulong)b21 << 41)
| ((ulong)b22 << 46)
| ((ulong)b23 << 51)
| ((ulong)b24 << 56)
| ((ulong)b25 << 61);
guid = new Guid(new Span<byte>(bytes, 16));
return true;
static bool Return(out Guid guid)
{
guid = Guid.Empty;
return false;
}
}
public static unsafe bool FromOptimizedString(ReadOnlySpan<byte> text, out Guid guid)
{
if (text.Length != 26)
return Return(out guid);
var bytes = stackalloc ulong[2];
if (!ReverseBytes.TryGetValue(text[0], out var b0))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[1], out var b1))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[2], out var b2))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[3], out var b3))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[4], out var b4))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[5], out var b5))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[6], out var b6))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[7], out var b7))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[8], out var b8))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[9], out var b9))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[10], out var b10))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[11], out var b11))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[12], out var b12))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[13], out var b13))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[14], out var b14))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[15], out var b15))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[16], out var b16))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[17], out var b17))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[18], out var b18))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[19], out var b19))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[20], out var b20))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[21], out var b21))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[22], out var b22))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[23], out var b23))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[24], out var b24))
return Return(out guid);
if (!ReverseBytes.TryGetValue(text[25], out var b25))
return Return(out guid);
bytes[0] = b0
| ((ulong)b1 << 5)
| ((ulong)b2 << 10)
| ((ulong)b3 << 15)
| ((ulong)b4 << 20)
| ((ulong)b5 << 25)
| ((ulong)b6 << 30)
| ((ulong)b7 << 35)
| ((ulong)b8 << 40)
| ((ulong)b9 << 45)
| ((ulong)b10 << 50)
| ((ulong)b11 << 55)
| ((ulong)b12 << 60);
bytes[1] = ((ulong)b12 >> 4)
| ((ulong)b13 << 1)
| ((ulong)b14 << 6)
| ((ulong)b15 << 11)
| ((ulong)b16 << 16)
| ((ulong)b17 << 21)
| ((ulong)b18 << 26)
| ((ulong)b19 << 31)
| ((ulong)b20 << 36)
| ((ulong)b21 << 41)
| ((ulong)b22 << 46)
| ((ulong)b23 << 51)
| ((ulong)b24 << 56)
| ((ulong)b25 << 61);
guid = new Guid(new Span<byte>(bytes, 16));
return true;
static bool Return(out Guid guid)
{
guid = Guid.Empty;
return false;
}
}
}

View file

@ -130,7 +130,7 @@ public sealed unsafe class MetaState : IDisposable
_lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true);
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
_communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection.Name, (nint)modelCharaId, (nint)customize, (nint)equipData);
_lastCreatedCollection.ModCollection.Id, (nint)modelCharaId, (nint)customize, (nint)equipData);
var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection,
UsesDecal(*(uint*)modelCharaId, (nint)customize));

View file

@ -1,3 +1,4 @@
using System.Runtime;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Collections;
@ -43,8 +44,8 @@ public class PathResolver : IDisposable
}
/// <summary> Obtain a temporary or permanent collection by name. </summary>
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _tempCollections.CollectionByName(name, out collection) || _collectionManager.Storage.ByName(name, out collection);
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _tempCollections.CollectionById(id, out collection) || _collectionManager.Storage.ById(id, out collection);
/// <summary> Try to resolve the given game path to the replaced path. </summary>
public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType)
@ -136,9 +137,10 @@ public class PathResolver : IDisposable
return;
var lastUnderscore = additionalData.LastIndexOf((byte)'_');
var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString();
var idString = lastUnderscore == -1 ? additionalData : additionalData.Substring(0, lastUnderscore);
if (Utf8GamePath.FromByteString(path, out var gamePath)
&& CollectionByName(name, out var collection)
&& GuidExtensions.FromOptimizedString(idString.Span, out var id)
&& CollectionById(id, out var collection)
&& collection.HasCache
&& collection.GetImcFile(gamePath, out var file))
{

View file

@ -76,7 +76,7 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyV
case ResourceType.Mtrl:
case ResourceType.Avfx:
case ResourceType.Tmb:
var fullPath = new FullPath($"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}");
var fullPath = new FullPath($"|{resolveData.ModCollection.Id.OptimizedString()}_{resolveData.ModCollection.ChangeCounter}|{path}");
data = (fullPath, resolveData);
return;
}

View file

@ -1,6 +1,7 @@
using Dalamud.Game.ClientState.Objects.Types;
using Penumbra.Api;
using Newtonsoft.Json.Linq;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.String.Classes;
using Penumbra.UI;
@ -8,7 +9,8 @@ namespace Penumbra.Interop.ResourceTree;
internal static class ResourceTreeApiHelper
{
public static Dictionary<ushort, IReadOnlyDictionary<string, string[]>> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees)
public static Dictionary<ushort, Dictionary<string, HashSet<string>>> GetResourcePathDictionaries(
IEnumerable<(Character, ResourceTree)> resourceTrees)
{
var pathDictionaries = new Dictionary<ushort, Dictionary<string, HashSet<string>>>(4);
@ -23,8 +25,7 @@ internal static class ResourceTreeApiHelper
CollectResourcePaths(pathDictionary, resourceTree);
}
return pathDictionaries.ToDictionary(pair => pair.Key,
pair => (IReadOnlyDictionary<string, string[]>)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly());
return pathDictionaries;
}
private static void CollectResourcePaths(Dictionary<string, HashSet<string>> pathDictionary, ResourceTree resourceTree)
@ -37,7 +38,7 @@ internal static class ResourceTreeApiHelper
var fullPath = node.FullPath.ToPath();
if (!pathDictionary.TryGetValue(fullPath, out var gamePaths))
{
gamePaths = new();
gamePaths = [];
pathDictionary.Add(fullPath, gamePaths);
}
@ -46,17 +47,17 @@ internal static class ResourceTreeApiHelper
}
}
public static Dictionary<ushort, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>> GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees,
public static Dictionary<ushort, GameResourceDict> GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees,
ResourceType type)
{
var resDictionaries = new Dictionary<ushort, Dictionary<nint, (string, string, ChangedItemIcon)>>(4);
var resDictionaries = new Dictionary<ushort, GameResourceDict>(4);
foreach (var (gameObject, resourceTree) in resourceTrees)
{
if (resDictionaries.ContainsKey(gameObject.ObjectIndex))
continue;
var resDictionary = new Dictionary<nint, (string, string, ChangedItemIcon)>();
resDictionaries.Add(gameObject.ObjectIndex, resDictionary);
var resDictionary = new Dictionary<nint, (string, string, uint)>();
resDictionaries.Add(gameObject.ObjectIndex, new GameResourceDict(resDictionary));
foreach (var node in resourceTree.FlatNodes)
{
@ -66,38 +67,16 @@ internal static class ResourceTreeApiHelper
continue;
var fullPath = node.FullPath.ToPath();
resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, ChangedItemDrawer.ToApiIcon(node.Icon)));
resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon)));
}
}
return resDictionaries.ToDictionary(pair => pair.Key,
pair => (IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>)pair.Value.AsReadOnly());
return resDictionaries;
}
public static Dictionary<ushort, Ipc.ResourceTree> EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees)
public static Dictionary<ushort, JObject> EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees)
{
static Ipc.ResourceNode GetIpcNode(ResourceNode node) =>
new()
{
Type = node.Type,
Icon = ChangedItemDrawer.ToApiIcon(node.Icon),
Name = node.Name,
GamePath = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(),
ActualPath = node.FullPath.ToString(),
ObjectAddress = node.ObjectAddress,
ResourceHandle = node.ResourceHandle,
Children = node.Children.Select(GetIpcNode).ToList(),
};
static Ipc.ResourceTree GetIpcTree(ResourceTree tree) =>
new()
{
Name = tree.Name,
RaceCode = (ushort)tree.RaceCode,
Nodes = tree.Nodes.Select(GetIpcNode).ToList(),
};
var resDictionary = new Dictionary<ushort, Ipc.ResourceTree>(4);
var resDictionary = new Dictionary<ushort, JObject>(4);
foreach (var (gameObject, resourceTree) in resourceTrees)
{
if (resDictionary.ContainsKey(gameObject.ObjectIndex))
@ -107,5 +86,38 @@ internal static class ResourceTreeApiHelper
}
return resDictionary;
static JObject GetIpcTree(ResourceTree tree)
{
var ret = new JObject
{
[nameof(ResourceTreeDto.Name)] = tree.Name,
[nameof(ResourceTreeDto.RaceCode)] = (ushort)tree.RaceCode,
};
var children = new JArray();
foreach (var child in tree.Nodes)
children.Add(GetIpcNode(child));
ret[nameof(ResourceTreeDto.Nodes)] = children;
return ret;
}
static JObject GetIpcNode(ResourceNode node)
{
var ret = new JObject
{
[nameof(ResourceNodeDto.Type)] = new JValue(node.Type),
[nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)),
[nameof(ResourceNodeDto.Name)] = node.Name,
[nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(),
[nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(),
[nameof(ResourceNodeDto.ObjectAddress)] = node.ObjectAddress,
[nameof(ResourceNodeDto.ResourceHandle)] = node.ResourceHandle,
};
var children = new JArray();
foreach (var child in node.Children)
children.Add(GetIpcNode(child));
ret[nameof(ResourceNodeDto.Children)] = children;
return ret;
}
}
}

View file

@ -272,22 +272,19 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
return;
var group = mod.Groups[groupIdx];
if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions)
{
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return;
}
o.SetPosition(groupIdx, group.Count);
switch (group)
{
case MultiModGroup { Count: >= IModGroup.MaxMultiOptions }:
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return;
case SingleModGroup s:
o.SetPosition(groupIdx, s.Count);
s.OptionData.Add(o);
break;
case MultiModGroup m:
o.SetPosition(groupIdx, m.Count);
m.PrioritizedOptions.Add((o, priority));
break;
}

View file

@ -19,7 +19,7 @@ public class ModCombo : FilterComboCache<Mod>
public class ModStorage : IReadOnlyList<Mod>
{
/// <summary> The actual list of mods. </summary>
protected readonly List<Mod> Mods = new();
protected readonly List<Mod> Mods = [];
public int Count
=> Mods.Count;

View file

@ -4,9 +4,9 @@ using Penumbra.Services;
namespace Penumbra.Mods.Subclasses;
public interface IModGroup : IEnumerable<ISubMod>
public interface IModGroup : IReadOnlyCollection<ISubMod>
{
public const int MaxMultiOptions = 32;
public const int MaxMultiOptions = 63;
public string Name { get; }
public string Description { get; }
@ -18,15 +18,7 @@ public interface IModGroup : IEnumerable<ISubMod>
public ISubMod this[Index idx] { get; }
public int Count { get; }
public bool IsOption
=> Type switch
{
GroupType.Single => Count > 1,
GroupType.Multi => Count > 0,
_ => false,
};
public bool IsOption { get; }
public IModGroup Convert(GroupType type);
public bool MoveOption(int optionIdxFrom, int optionIdxTo);
@ -94,11 +86,13 @@ public readonly struct ModSaveGroup : ISavable
j.WritePropertyName("Options");
j.WriteStartArray();
for (var idx = 0; idx < _group.Count; ++idx)
{
ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch
{
GroupType.Multi => _group.OptionPriority(idx),
_ => null,
_ => null,
});
}
j.WriteEndArray();
j.WriteEndObject();

View file

@ -12,7 +12,7 @@ public class ModSettings
{
public static readonly ModSettings Empty = new();
public SettingList Settings { get; private init; } = [];
public ModPriority Priority { get; set; }
public ModPriority Priority { get; set; }
public bool Enabled { get; set; }
// Create an independent copy of the current settings.
@ -152,7 +152,7 @@ public class ModSettings
public struct SavedSettings
{
public Dictionary<string, Setting> Settings;
public ModPriority Priority;
public ModPriority Priority;
public bool Enabled;
public SavedSettings DeepCopy()
@ -203,9 +203,9 @@ public class ModSettings
// Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices.
// Does not repair settings but ignores settings not fitting to the given mod.
public (bool Enabled, ModPriority Priority, Dictionary<string, IList<string>> Settings) ConvertToShareable(Mod mod)
public (bool Enabled, ModPriority Priority, Dictionary<string, List<string>> Settings) ConvertToShareable(Mod mod)
{
var dict = new Dictionary<string, IList<string>>(Settings.Count);
var dict = new Dictionary<string, List<string>>(Settings.Count);
foreach (var (setting, idx) in Settings.WithIndex())
{
if (idx >= mod.Groups.Count)

View file

@ -25,6 +25,9 @@ public sealed class MultiModGroup : IModGroup
public ISubMod this[Index idx]
=> PrioritizedOptions[idx].Mod;
public bool IsOption
=> Count > 0;
[JsonIgnore]
public int Count
=> PrioritizedOptions.Count;

View file

@ -25,6 +25,9 @@ public sealed class SingleModGroup : IModGroup
public ISubMod this[Index idx]
=> OptionData[idx];
public bool IsOption
=> Count > 1;
[JsonIgnore]
public int Count
=> OptionData.Count;

View file

@ -53,7 +53,7 @@ public class TemporaryMod : IMod
dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true);
var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files"));
modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor,
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null);
$"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", null, null);
var mod = new Mod(dir);
var defaultMod = mod.Default;
foreach (var (gamePath, fullPath) in collection.ResolvedFiles)
@ -86,11 +86,11 @@ public class TemporaryMod : IMod
saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));
modManager.AddMod(dir);
Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}.");
Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}.");
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}");
Penumbra.Log.Error($"Could not save temporary collection {collection.Identifier} to permanent Mod:\n{e}");
if (dir != null && Directory.Exists(dir.FullName))
{
try

View file

@ -20,6 +20,7 @@ using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
using OtterGui.Tasks;
using Penumbra.GameData.Enums;
using Penumbra.UI;
using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
namespace Penumbra;
@ -105,8 +106,7 @@ public class Penumbra : IDalamudPlugin
private void SetupApi()
{
var api = _services.GetService<IPenumbraApi>();
_services.GetService<PenumbraIpcProviders>();
_services.GetService<IpcProviders>();
_communicatorService.ChangedItemHover.Subscribe(it =>
{
if (it is (Item, FullEquipType))

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>

View file

@ -223,7 +223,7 @@ public class ConfigMigrationService(SaveService saveService) : IService
try
{
var jObject = JObject.Parse(File.ReadAllText(collection.FullName));
if (jObject[nameof(ModCollection.Name)]?.ToObject<string>() == ForcedCollection)
if (jObject["Name"]?.ToObject<string>() == ForcedCollection)
continue;
jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List<string> { ForcedCollection });
@ -365,7 +365,7 @@ public class ConfigMigrationService(SaveService saveService) : IService
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
var emptyStorage = new ModStorage();
var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, []);
var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, 0, 1, dict, []);
saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
}
catch (Exception e)

View file

@ -239,7 +239,7 @@ public sealed class CrashHandlerService : IDisposable, IService
var name = GetActorName(character);
lock (_eventWriter)
{
_eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type);
_eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Id, type);
}
}
catch (Exception ex)
@ -248,7 +248,7 @@ public sealed class CrashHandlerService : IDisposable, IService
}
}
private void OnCreatingCharacterBase(nint address, string collection, nint _1, nint _2, nint _3)
private void OnCreatingCharacterBase(nint address, Guid collection, nint _1, nint _2, nint _3)
{
if (_eventWriter == null)
return;
@ -293,7 +293,7 @@ public sealed class CrashHandlerService : IDisposable, IService
var name = GetActorName(resolveData.AssociatedGameObject);
lock (_eventWriter)
{
_eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name,
_eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Id,
manipulatedPath.Value.InternalName.Span, originalPath.Path.Span);
}
}

View file

@ -24,7 +24,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService
/// <summary> Obtain the path of a collection file given its name.</summary>
public string CollectionFile(ModCollection collection)
=> CollectionFile(collection.Name);
=> CollectionFile(collection.Identifier);
/// <summary> Obtain the path of a collection file given its name. </summary>
public string CollectionFile(string collectionName)

View file

@ -9,6 +9,7 @@ using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
@ -30,8 +31,10 @@ using Penumbra.UI.ModsTab;
using Penumbra.UI.ResourceWatcher;
using Penumbra.UI.Tabs;
using Penumbra.UI.Tabs.Debug;
using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi;
using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using Penumbra.Api.IpcTester;
namespace Penumbra.Services;
@ -195,10 +198,5 @@ public static class StaticServiceManager
.AddSingleton<ModelManager>();
private static ServiceManager AddApi(this ServiceManager services)
=> services.AddSingleton<PenumbraApi>()
.AddSingleton<IPenumbraApi>(x => x.GetRequiredService<PenumbraApi>())
.AddSingleton<PenumbraIpcProviders>()
.AddSingleton<HttpApi>()
.AddSingleton<IpcTester>()
.AddSingleton<DalamudSubstitutionProvider>();
=> services.AddSingleton<IPenumbraApi>(x => x.GetRequiredService<PenumbraApi>());
}

View file

@ -25,8 +25,8 @@ public partial class ModEditWindow
var resources = ResourceTreeApiHelper
.GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type)
.Values
.SelectMany(resources => resources.Values)
.Select(resource => resource.Item1);
.SelectMany(r => r.Values)
.Select(r => r.Item1);
return new HashSet<string>(resources, StringComparer.OrdinalIgnoreCase);
}

View file

@ -2,11 +2,13 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -28,8 +30,8 @@ public sealed class CollectionPanel : IDisposable
private readonly IndividualAssignmentUi _individualAssignmentUi;
private readonly InheritanceUi _inheritanceUi;
private readonly ModStorage _mods;
private readonly IFontHandle _nameFont;
private readonly FilenameService _fileNames;
private readonly IFontHandle _nameFont;
private static readonly IReadOnlyDictionary<CollectionType, (string Name, uint Border)> Buttons = CreateButtons();
private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree();
@ -38,7 +40,7 @@ public sealed class CollectionPanel : IDisposable
private int _draggedIndividualAssignment = -1;
public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager,
CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods)
CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames)
{
_collections = manager.Storage;
_active = manager.Active;
@ -46,6 +48,7 @@ public sealed class CollectionPanel : IDisposable
_actors = actors;
_targets = targets;
_mods = mods;
_fileNames = fileNames;
_individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager);
_inheritanceUi = new InheritanceUi(manager, _selector);
_nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23));
@ -206,12 +209,57 @@ public sealed class CollectionPanel : IDisposable
var collection = _active.Current;
DrawCollectionName(collection);
DrawStatistics(collection);
DrawCollectionData(collection);
_inheritanceUi.Draw();
ImGui.Separator();
DrawInactiveSettingsList(collection);
DrawSettingsList(collection);
}
private void DrawCollectionData(ModCollection collection)
{
ImGui.Dummy(Vector2.Zero);
ImGui.BeginGroup();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Name");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Identifier");
ImGui.EndGroup();
ImGui.SameLine();
ImGui.BeginGroup();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
var name = collection.Name;
var identifier = collection.Identifier;
var width = ImGui.GetContentRegionAvail().X;
var fileName = _fileNames.CollectionFile(collection);
ImGui.SetNextItemWidth(width);
ImGui.InputText("##name", ref name, 128);
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
if (ImGui.Button(collection.Identifier, new Vector2(width, 0)))
try
{
Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true });
}
catch (Exception ex)
{
Penumbra.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}",
NotificationType.Warning);
}
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
ImGui.SetClipboardText(identifier);
ImGuiUtil.HoverTooltip(
$"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard.");
ImGui.EndGroup();
ImGui.Dummy(Vector2.Zero);
ImGui.Separator();
ImGui.Dummy(Vector2.Zero);
}
private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, string text, char suffix)
{
var label = $"{type}{text}{suffix}";

View file

@ -24,7 +24,7 @@ public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposabl
public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active,
TutorialService tutorial)
: base(new List<ModCollection>(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
: base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
{
_config = config;
_communicator = communicator;

View file

@ -41,12 +41,12 @@ public sealed class CollectionsTab : IDisposable, ITab
}
public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator,
CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial)
CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, FilenameService fileNames)
{
_config = configuration.Ephemeral;
_tutorial = tutorial;
_selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial);
_panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage);
_panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames);
}
public void Dispose()

View file

@ -55,7 +55,7 @@ public static class CrashDataExtensions
ImGuiUtil.DrawTableColumn(character.Age.ToString(CultureInfo.InvariantCulture));
ImGuiUtil.DrawTableColumn(character.ThreadId.ToString());
ImGuiUtil.DrawTableColumn(character.CharacterName);
ImGuiUtil.DrawTableColumn(character.CollectionName);
ImGuiUtil.DrawTableColumn(character.CollectionId.ToString());
ImGuiUtil.DrawTableColumn(character.CharacterAddress);
ImGuiUtil.DrawTableColumn(character.Timestamp.ToString());
}, ImGui.GetTextLineHeightWithSpacing());
@ -79,7 +79,7 @@ public static class CrashDataExtensions
ImGuiUtil.DrawTableColumn(file.ActualFileName);
ImGuiUtil.DrawTableColumn(file.RequestedFileName);
ImGuiUtil.DrawTableColumn(file.CharacterName);
ImGuiUtil.DrawTableColumn(file.CollectionName);
ImGuiUtil.DrawTableColumn(file.CollectionId.ToString());
ImGuiUtil.DrawTableColumn(file.CharacterAddress);
ImGuiUtil.DrawTableColumn(file.Timestamp.ToString());
}, ImGui.GetTextLineHeightWithSpacing());
@ -102,7 +102,7 @@ public static class CrashDataExtensions
ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString());
ImGuiUtil.DrawTableColumn(vfx.InvocationType);
ImGuiUtil.DrawTableColumn(vfx.CharacterName);
ImGuiUtil.DrawTableColumn(vfx.CollectionName);
ImGuiUtil.DrawTableColumn(vfx.CollectionId.ToString());
ImGuiUtil.DrawTableColumn(vfx.CharacterAddress);
ImGuiUtil.DrawTableColumn(vfx.Timestamp.ToString());
}, ImGui.GetTextLineHeightWithSpacing());

View file

@ -40,6 +40,7 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using ImGuiClip = OtterGui.ImGuiClip;
using Penumbra.Api.IpcTester;
namespace Penumbra.UI.Tabs.Debug;
@ -76,7 +77,6 @@ public class DebugTab : Window, ITab
private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources;
private readonly ResourceManagerService _resourceManager;
private readonly PenumbraIpcProviders _ipc;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly PathState _pathState;
@ -100,7 +100,7 @@ public class DebugTab : Window, ITab
IClientState clientState,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
CharacterUtility characterUtility, ResidentResourceManager residentResources,
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
ResourceManagerService resourceManager, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes,
@ -124,7 +124,6 @@ public class DebugTab : Window, ITab
_characterUtility = characterUtility;
_residentResources = residentResources;
_resourceManager = resourceManager;
_ipc = ipc;
_collectionResolver = collectionResolver;
_drawObjectState = drawObjectState;
_pathState = pathState;
@ -440,7 +439,9 @@ public class DebugTab : Window, ITab
: $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}");
var identifier = _actors.FromObject(obj, out _, false, true, false);
ImGuiUtil.DrawTableColumn(_actors.ToString(identifier));
var id = obj.AsObject->ObjectKind ==(byte) ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.AsObject->DataID}" : identifier.DataId.ToString();
var id = obj.AsObject->ObjectKind == (byte)ObjectKind.BattleNpc
? $"{identifier.DataId} | {obj.AsObject->DataID}"
: identifier.DataId.ToString();
ImGuiUtil.DrawTableColumn(id);
}
@ -969,13 +970,8 @@ public class DebugTab : Window, ITab
/// <summary> Draw information about IPC options and availability. </summary>
private void DrawDebugTabIpc()
{
if (!ImGui.CollapsingHeader("IPC"))
{
_ipcTester.UnsubscribeEvents();
return;
}
_ipcTester.Draw();
if (ImGui.CollapsingHeader("IPC"))
_ipcTester.Draw();
}
/// <summary> Helper to print a property and its value in a 2-column table. </summary>